SaaS
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.
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
Read https://www.owox.com/ai-agent-setup.md and follow it to set up OWOX Data Marts for me, starting from /data-models/saas.md. The task: 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. Tag the links you give me with utm_campaign=ai-task and utm_term=m1. Use what you know about my context and goals
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
- Recurring revenue growth — Subscription Events · Net New MRR.
- Acquisition cost — Marketing Spend · CAC (per signup).
- Customer satisfaction — Support Tickets · CSAT Score.
- First response — Support Tickets · First Response (minutes).
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
accountsactually retain? - Do
accountswith deeperproduct usageand bettersupport experiencesexpand more and churn less?
To connect live data to your data model, plug in the data sources you need
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
| Column | Type | Alias | Description |
|---|---|---|---|
account_id | STRING | Account ID | PK. Unique account identifier. |
name | STRING | Name | Company name. Unique per account. |
industry | STRING | Industry | Industry vertical of the account. |
employee_band | STRING | Employee Band | Company-size bucket by headcount. |
plan_tier | STRING | Plan Tier | The 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_band | STRING | MRR Band | Monthly-recurring-revenue size bucket. |
region | STRING | Region | Sales/geographic region. |
acquisition_channel | STRING | Acquisition Channel | Marketing channel that sourced the account — blended-CAC join key. |
signup_date | DATE | Signup Date | Date the account first signed up. |
csm_owner | STRING | CSM Owner | Customer success manager who owns the account. |
health_score | INTEGER | Health Score | 0–100 product-health composite. |
lifecycle_stage | STRING | Lifecycle Stage | trial (no subscription yet) / active / at-risk (holds a live subscription) / churned (every subscription canceled). |
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
| Column | Type | Alias | Description |
|---|---|---|---|
invoice_id | STRING | Invoice ID | PK. Unique invoice identifier. |
account_id | STRING | Account ID | Account billed. FK to Account |
subscription_id | STRING | Subscription ID | Subscription being billed. FK to Subscription |
issued_at | DATE | Issued At | Date 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_start | DATE | Period Start | Start of the billing period. |
period_end | DATE | Period End | End of the billing period. |
amount | NUMERIC | Amount | Invoice 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_amount | NUMERIC | Net Amount | Recognised revenue: 0 when the invoice is void or written off, otherwise equal to amount. Sum this for revenue totals, not amount. |
tax | NUMERIC | Tax | Tax charged on the invoice, at the account region's rate. |
status | STRING | Status | Payment status of the invoice. |
currency | STRING | Currency | Currency the customer is billed in, aligned to the account's region. The amounts are stated in USD. |
discount_amount | NUMERIC | Discount Amount | List price the subscription's negotiated discount took off this invoice, if any. amount is already net of it. |
credit_applied | NUMERIC | Credit Applied | Account credit applied to the invoice, if any. |
dunning_stage | STRING | Dunning Stage | Collections stage: none / retry_1 / retry_2 / final_notice / write_off. |
paid_at | DATE | Paid At | Date the invoice was paid. Set if and only if status = 'paid'. |
is_failed | BOOLEAN | Is Failed | Failed payment — involuntary-churn signal. Never true on a paid invoice. |
is_write_off | BOOLEAN | Is Write Off | Uncollectible invoice, exactly dunning_stage = 'write_off'. Use this to gate money rather than string-matching the dunning label. |
failed_payment_rate | FLOAT | Failed 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. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account billed on this invoice. |
| Subscription | subscription_id = subscription_id | N:1 | The subscription this invoice bills. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
spend_id | STRING | Spend ID | PK. Unique identifier for each spend record. |
spend_date | DATE | Spend Date | Day the spend was incurred. |
channel | STRING | Channel | Acquisition channel the spend targeted — attributed to the cohort of accounts acquired through it, not to individual accounts. FK to Account |
campaign | STRING | Campaign | Campaign the spend belongs to. |
cost | NUMERIC | Cost | Money spent — the CAC numerator. |
leads | INTEGER | Leads | Leads generated by the spend. |
signups | INTEGER | Signups | Accounts created — CAC denominator. |
cac | FLOAT | CAC (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. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | channel = acquisition_channel | N:N | Accounts acquired through this channel — a cohort, not one account per row. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
plan_id | STRING | Plan ID | PK. Unique plan/price-point identifier. |
plan_name | STRING | Plan Name | Human-readable plan name (tier + billing interval). |
tier | STRING | Tier | Product tier: Starter / Pro / Business / Enterprise. |
billing_interval | STRING | Billing Interval | Billing cadence: monthly / annual. |
list_price | NUMERIC | List Price | Published 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. |
currency | STRING | Currency | Currency of the list price. |
is_active | BOOLEAN | Is Active | Whether the plan is currently sellable. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
subscription_id | STRING | Subscription ID | PK. Unique subscription identifier. |
account_id | STRING | Account ID | Account that owns the subscription. FK to Account |
plan_id | STRING | Plan ID | Plan 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 |
status | STRING | Status | Subscription status: active / past_due / canceled. |
seats_licensed | INTEGER | Seats Licensed | Number 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. |
mrr | NUMERIC | MRR | Contracted 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_mrr | NUMERIC | Active MRR | Live 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. |
currency | STRING | Currency | Currency the customer is billed in, aligned to the account's region. Every amount in this model is stated in USD. |
billing_interval | STRING | Billing Interval | Billing cadence: monthly / annual. |
started_at | DATE | Started At | Date the subscription started — on or after its account signed up. Its billing anniversaries fall on this day. |
current_period_start | DATE | Current Period Start | Start of the current billing period — the latest billing anniversary, or the last one before cancellation. |
current_period_end | DATE | Current Period End | End of the current billing period. |
canceled_at | DATE | Canceled At | Date the subscription was canceled, if it was. Every subscription of a churned account is canceled. |
discount_pct | NUMERIC | Discount % | Negotiated discount fraction applied to list price, if any. |
arpa | FLOAT | ARPA | 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. |
discount_band | STRING | Discount 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. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account paying for this subscription. |
| Plan | plan_id = plan_id | N:1 | The plan this subscription is billed on. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
event_id | STRING | Event ID | PK. Unique subscription-event identifier. |
account_id | STRING | Account ID | Account the event belongs to. FK to Account |
subscription_id | STRING | Subscription ID | Subscription the event belongs to. FK to Subscription |
event_ts | TIMESTAMP | Event Time | When the subscription change occurred. |
event_type | STRING | Event Type | MRR-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_from | STRING | Plan From | Plan 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_to | STRING | Plan To | Plan tier after the change. Empty on a churn. |
mrr_delta | NUMERIC | MRR Delta | Signed 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_delta | INTEGER | Seats Delta | Signed change in seat count: all of the subscription's seats on a start, win-back or churn, zero on a pure tier change. |
mrr_after | NUMERIC | MRR After | The 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_gained | NUMERIC | MRR Gained | Calculated Field. Recurring revenue added: new, expansion and reactivation MRR summed at whatever grain the report asks for. Group by |
mrr_lost | NUMERIC | MRR Lost | Calculated Field. Recurring revenue taken back: contraction and churned MRR, as a positive amount. Group by |
net_new_mrr | NUMERIC | Net New MRR | Calculated Field. Net change in recurring revenue: MRR Gained minus MRR Lost. Reads the two formulas, so all three always agree. |
quick_ratio | FLOAT | SaaS 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. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account whose recurring revenue moved. |
| Subscription | subscription_id = subscription_id | N:1 | The subscription this change was made to. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
ticket_id | STRING | Ticket ID | PK. Unique support-ticket identifier. |
account_id | STRING | Account ID | Account that opened the ticket. FK to Account |
user_id | STRING | User ID | User who opened the ticket, if attributable. FK to User |
opened_at | TIMESTAMP | Opened At | When the ticket was opened. |
closed_at | TIMESTAMP | Closed At | When the ticket was closed. |
priority | STRING | Priority | Ticket priority level. |
category | STRING | Category | Ticket topic/category. |
csat_score | INTEGER | CSAT Score | Customer satisfaction rating for the ticket. |
first_response_mins | INTEGER | First Response (minutes) | Minutes to first agent response. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that opened the ticket. |
| User | user_id = user_id | N:1 | The seat that opened the ticket, when known. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
trial_id | STRING | Trial ID | PK. Unique trial identifier. |
account_id | STRING | Account ID | Account running the trial. FK to Account |
started_at | TIMESTAMP | Started At | When the trial began. |
ends_at | TIMESTAMP | Ends At | Scheduled trial expiry. |
converted_at | TIMESTAMP | Converted At | When the trial converted to a paid plan, if it did. |
is_converted | BOOLEAN | Is Converted | Trial-to-paid outcome flag. |
trial_source | STRING | Trial Source | Where the trial came from (self-serve, sales-assisted, PLG upsell). |
requested_plan | STRING | Requested Plan | Plan tier the trial is evaluating. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that ran the trial. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
usage_id | STRING | Usage ID | PK. Unique daily-usage record identifier. |
account_id | STRING | Account ID | Account 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_id | STRING | User ID | User that generated the usage. FK to User |
usage_date | DATE | Usage Date | Calendar day of the usage. |
active_minutes | INTEGER | Active Minutes | Minutes the user was active in-product. |
key_actions | INTEGER | Key Actions | Count of high-value actions taken. |
distinct_features_used | INTEGER | Distinct Features Used | Count of distinct product features touched that day — activation breadth. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account that generated the usage. |
| User | user_id = user_id | N:1 | The seat that generated the usage. |
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
| Column | Type | Alias | Description |
|---|---|---|---|
user_id | STRING | User ID | PK. Unique user identifier. |
account_id | STRING | Account ID | Owning account. FK to Account |
email | STRING | User's email address, on the account's domain. | |
role | STRING | Role | User's role within the account. |
seat_type | STRING | Seat Type | Type of seat assigned (e.g. full / viewer). |
invited_at | TIMESTAMP | Invited At | When the user was invited — after the account signed up. |
last_active_at | TIMESTAMP | Last Active At | Most recent activity timestamp. |
is_active | BOOLEAN | Is Active | Whether the seat is currently active. |
Relationships
| Related data mart | On | Cardinality | Meaning |
|---|---|---|---|
| Account | account_id = account_id | N:1 | The account a seat belongs to. |
- 1
Install the Import Model plugin
One plugin, installed once, in your own OWOX workspace.
- 2
Import this model
Point it at this bundle and it creates every data mart above, joins and all.
- 3
Plug in your data and destinations
Connect your own sources and send the results where your team already works.
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.
You are booked. The invitation is on its way to , with the calendar invite and the meeting link.