SaaS and technology · History

State history

How every subscription, account, owner and usage indicator looked on any given day, stored before billing, CRM and product overwrite or delete the past.

UsageCommon
EffortMedium
ForSaaS companies that need to answer how the base looked on a past day: finance, RevOps and CS.
Source

Billing, CRM, product and analytics

Rule

SCD at the source and an accumulator for what gets deleted

Outcome

The state of any entity on any day

Activation

MRR, commissions, health score and AI agent

What it solves

  • MRR, books and status for past months stop changing with today's state.
  • Usage and events that the source deletes are kept.
  • A sync that fails does not take the history with it.

Business questions it answers

Ask your agent these questions once the case is live.

What was customer X's subscription status on June 30?
Who owned this account in March?
Which customers asked to cancel at the end of the cycle and later changed their minds?
How did this account's usage evolve over the last 12 weeks?
Since when do we have history for each table?

What you need

  • Billing, CRM and product connected to Nekt
  • SCD Type 2 turned on for tables that change in place
  • The list of business fields worth tracking

What you get

  • Versioned tables with validity start and end
  • Daily snapshots of usage and calculated metrics
  • A coverage map with the start date of each history
  • An AI agent that answers how things looked on a date

Where to activate

Close and retroactive MRR

MRR, active customers and movements for any month, read from that month's state.

Books and commissions

Owner of the month and books by cutoff day, for NDR and variable pay without disputes.

AI agent via MCP

Answers how an account looked on a date, stating where the data comes from and since when history exists.

Ready to build?

Turning on SCD is one setting per table. Accumulators for sources that delete data need a notebook. The biggest cost is starting late. The detailed view has the business rules, the pitfalls and the acceptance test. Or send everything straight to your agent with the Nekt MCP.

The problem

Billing, CRM and product keep only the current state. The subscription that was past_due yesterday shows as active today, the owner of the deal that closed is now someone else, analytics' 30-day usage is overwritten on every sync, and Stripe events and TTL collections disappear after a few weeks. Without stored history there is no MRR for past months, no commission by owner of the month and no usage trend. And a full sync that fails halfway can wipe out months.

How it is usually done, and the rule

Each row is a mistake that already happened in practice.

The common wayThe rule
01
Calculating the past with today's table.
Turn on history for state tables, starting now

Enable Nekt's SCD Type 2 on tables that change in place: billing subscriptions and items, CRM customers, deals and companies, product accounts and plans. Every change becomes a version with _available_from, _available_until and _deleted. What came before does not come back: for MRR, rebuild from invoices; for owner and stage, from the CRM property history.

02
Trusting the source to keep event and usage history.
A source that deletes needs an accumulator

Stripe events are available for about 30 to 40 days, MongoDB collections with a TTL delete documents (about 45 days in a real case) and Amplitude charts and metric spreadsheets return only an overwritten 30-day window. Accumulate into your own table by day and id, with merge, reprocessing the last few days on each run so a failure heals itself.

03
A SQL query that recalculates the whole series from today's state.
A snapshot of a calculated number is a notebook, not SQL

A SQL transformation on Nekt overwrites the whole table on every run. To snapshot daily MRR, books or aggregated usage, use a notebook with mode merge, or a query that reads its own output and keeps all previous days, replacing only the current day.

04
Writing whatever comes back, even if it is empty.
Never overwrite with empty

Before writing, the routine refuses an empty result or one much smaller than the previous one, unless explicitly allowed. Before a full sync of a large source, take a frozen copy: a full sync that failed halfway once wiped almost five months of events from a table.

05
Counting every new version as a subscription change.
A new version is not a business event

In billing, most new versions only advance the billing cycle or swap the latest invoice. To count a change in status, plan, quantity, price or end-of-cycle cancellation notice (cancel_at_period_end), compare the business field with the previous version and count only when it changed.

06
Cutting the day in UTC, or converting the time zone twice.
Day cutoff in the business time zone

The state on day D is the version valid at D+1 00:00 Brasília time, that is, D+1 03:00 UTC. With several versions on the same day, the last one wins. Run the session in UTC and convert only at the cutoff, once.

07
Assigning the whole history to today's owner.
The owner of the month has a cutoff day

For books, NDR and commissions, the owner in a past month is the owner valid on an agreed day (for example, the 5th, giving the team days 1 to 4 to transfer accounts). Changes after the cutoff are flagged as mid-month transfers, without changing the base owner. The current month uses the latest owner.

08
Reading only the property's current value.
CRM history comes from properties

When SCD was not turned on in time, recover stage, amount and owner from HubSpot's property history (propertiesWithHistory, one entry per change with value, date and source). If the connector does not bring the property you need, a temporary script fetches it from the API, flagged as temporary and with a removal date.

09
Mixing recorded and rebuilt history with no flag.
Rebuilt is not recorded

A past rebuilt from invoices, logs or a backfill is flagged with its origin. Models and indicators use only what was available at the end of the previous period (point-in-time). In a real case, contract attributes rebuilt by a backfill contaminated a churn model and had to be removed.

10
Recalculating closed months on every run.
A correction is an adjustment, never a rewrite

A closed month is read from the frozen snapshot. A historical reclassification is done once, documented with date and reason, and is not repeated on every run. A later correction goes in as an adjustment in the month it was found.

What to keep and how

Each kind of SaaS data has its own way of not losing the past.

Billing subscriptions and itemsSCD Type 2 at the source_available_from / _available_until
Stripe change events (30 to 40 days)Accumulator by event id
CRM deals, owner and stageSCD or property historypropertiesWithHistory
30-day usage from analytics or a spreadsheetDaily snapshot per account
Product collection with a TTLDaily merge by day and id
Daily MRR, books, scoreNotebook with merge or self-referencing query
Large table before a full syncManual frozen copy

Definitions

Current version
row with _available_until null and _deleted false
State on date D
version with _available_from up to D+1 03:00 UTC (midnight Brasília time) and _available_until null or later than that instant
Daily snapshot
one row per entity and day with the end-of-day state, rewritable only within the declared reprocessing window
Business change
version in which a declared field (status, plan, quantity, price, cancel_at_period_end, owner, stage) differs from the previous version
Owner of the month
owner valid on the month's cutoff day for closed months; the latest owner for the current month
History coverage
date of the first stored version or snapshot of each table; before it, only flagged reconstruction
Gap
a day with no stored snapshot in an accumulated source, listed and never filled with zero

Pitfalls that already happened

Months wiped by a full sync

A full sync of product events failed halfway and wiped almost five months from the table.

Frozen copy before any full sync of a large source.

Accumulated snapshot overwritten

One run overwrote the daily snapshot table and three weeks had to be rebuilt by hand.

Self-referencing query that only replaces the current day, plus a guard against empty results.

No trend in the health score

The product's conversations collection deleted documents older than 45 days, and the trend component could never be calculated.

Daily accumulator per customer from day one.

Usage that vanishes the next day

The metrics spreadsheet only held the last 30 days, overwritten daily; there was no way to tell whether 45% adoption was rising from 20% or falling from 80%.

Accumulated weekly or daily snapshot per account.

Owner history that never arrived

The connector did not bring deal owner history, and monthly books ended up with today's owner.

Temporary API script, flagged for removal once the connector brings the field.

Model trained on the future

Contract attributes filled by a backfill leaked later information into the training of a churn model.

Features only from what existed at the end of the previous period.

Validated on real data. In the history of Nekt's own Stripe subscriptions, about 7 in 10 new versions changed neither status, items nor the cancellation notice: almost all of them only advanced the billing cycle. Only about 2 in 10 changed the status. Counting versions as changes would inflate subscription events more than 3 times.

Acceptance test

The case is done when every item passes.

Agent package

Paste into Claude, ChatGPT or any client with the Nekt MCP connected.

Full Markdown
State history
- For any question about the past, use the versioned table or the daily snapshot, never the current-state table.
- State on date D = version with _available_from up to D+1 03:00 UTC and _available_until null or later. Current today = _available_until null and _deleted false.
- A new version is not a business change. Change = status, plan, quantity, price, cancel_at_period_end, owner or stage different from the previous version.
- Owner of the month = owner valid on day <cutoff day> of the month; in the current month, the latest owner.
- Coverage: <table> has history since <date>. Before that, data is rebuilt and flagged; say so in the answer.
- A day with no snapshot is a gap, not zero.
- A closed month comes from the frozen snapshot; do not recalculate.
Client exceptions:
- <client>: deal owner history comes from a temporary script until <date>.
- <client>: one-off reclassification done on <date> for reason <reason>; do not reapply.

Variations

Freeze periods

Customer freezes and deactivations rebuilt as periods (start and end) from automation events, with no overlap.

Versioned score

Lead score and health score stored daily, to measure whether the rule was right without rewriting what the team saw.

Basis for forecasting

Attributes per account and month calculated at the end of the previous month, to predict churn and NRR without leaking the future.