SaaSClerk Blog · How the offboarding checklist is built

Offboarding checklist: cancel the SaaS tools an employee leaves behind

Short answer: a departed employee does not cancel their own seats, and the person who pays the card often never logs into the tool. Write the tools down as a per-person list, close each one with a timestamp, and show recoverable spend only for prices you actually know.

The leak is structural, not careless. The seat is billed to a company card; the login belongs to a person who no longer works there; the renewal email goes to a mailbox someone disabled on the last day. Nobody receives a signal that says "this charge is now for nobody". The charge simply continues, quietly, until an audit happens to look in the right place.

Why a spreadsheet stops working

A spreadsheet of departed employees can record the intent to cancel. It cannot record that a specific tool was actually closed, by whom, and when. Once two people edit the same row, the answer to "did we cancel Notion for Alex?" becomes a matter of memory. Memory is the thing that left with Alex.

Store the list, not the intention

The minimum honest data model is two tables: one row per departing employee, one row per tool under that person. The parent row holds the person and an overall status; the child row holds the provider, an optional cancel link, the item status, and the timestamp when it was closed.

model OffboardingList {
  id             String   @id @default(uuid())
  organizationId String
  employeeName   String
  employeeEmail  String
  status         OffboardingStatus @default(PENDING)
  notes          String?
  items          OffboardingItem[]
}

model OffboardingItem {
  id                String   @id @default(uuid())
  offboardingListId String
  subscriptionId    String?
  providerName      String
  cancelUrl         String?
  status            OffboardingItemStatus @default(PENDING)
  completedAt       DateTime?
}

Three details carry the whole design. The item status is an explicit enum (open, closed, intentionally kept) rather than a free-text note. completedAt is written when the item closes, which turns the list into an audit trail instead of a to-do list. And subscriptionId is optional: a checklist that only accepts tools already in your inventory cannot cover the tool nobody tracked, which is exactly the kind of tool that keeps charging.

Close items, and recompute the parent

The parent status should never be typed in by hand. Derive it from the children each time one changes: any open item means in progress, no open items means complete, and nothing touched means pending. That derivation is cheap and keeps the two levels from disagreeing.

const open = items.filter((item) => item.status === 'PENDING').length
const listStatus =
  open === 0 ? 'COMPLETED' : open === items.length ? 'PENDING' : 'IN_PROGRESS'

Show recoverable money only when the price is known

A checklist can display a monthly figure, but only by summing prices that exist in the inventory. For each open item, look up its linked subscription and add its stored monthly cost. If an item has no price on file, it contributes nothing to the number and is not silently counted as zero.

This distinction matters on the screen. "$12.50 a month still recoverable" is a claim backed by two rows. A checklist that assigns an estimated price to an unknown tool teaches the reader to distrust every number on the page, including the correct ones.

Server rules that keep the list trustworthy

A checklist about people and money has to be scoped tightly. Four rules are enough for a first version:

A note is not a field

Every tool has a detail that fits nowhere: "seat count 3", "cancel via the workspace owner", "keep for the contractor". Without a dedicated column, those details belong in the parent note, appended one line per item, rather than dropped at the API boundary. Losing an operator's note is how a checklist loses its audience.

What this looks like in practice

The person responsible opens the checklist on the day of departure, picks the tools already in the subscription inventory, and adds the rest by hand with a cancel link. Over the following days each item is closed with one button, and the progress bar is the only status anyone needs to read. The list stays after the person is gone, which is the point: the record outlives the memory.

Inside SaaSClerk this is the offboarding checklist, built on two tables that were already in the database. No new schema, no new integration, no manual reconciliation at the end of the month.

Related reading

Price the leak you already have

Before building a process, measure the waste: the unused-seat calculator takes seat counts and prices and returns the monthly and yearly figure, with no account required.