Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Data Lifecycle - Lona Docs Log in

Data Lifecycle

Sheets and rows follow a lifecycle from creation through deletion. Understanding this lifecycle is important for building features that persist data correctly.

Sheets

Sheets are the top-level container. A sheet owns rows via the spreadsheet_to_rows junction table.

OperationEffect
CreateInserts into spreadsheets, generates ID
Soft deleteSets deleted_at, sheet hidden from listings
Hard deleteRemoves sheet AND cleans up orphaned rows

Hard delete triggers an orphan cleanup that removes rows not linked to any sheet (see Row Cleanup below).

Rows

Rows exist in two modes: sheet-linked and standalone.

Sheet-linked rows

Created with a sheetId. The system inserts the row into spreadsheet_rows and creates a link in spreadsheet_to_rows.

const row = await sheet.rows.add({
  type: "number",
  label: "Weight",
});

When a sheet-linked row is unlinked from its last sheet (and has no lookupKey), it is automatically soft-deleted.

Standalone rows

Created without a sheetId but with a required lookupKey. These rows are not linked to any sheet and exist independently.

await sdk.rows.create({
  type: "scheduling-link",
  label: "Coffee Chat",
  lookupKey: ":bookings:coffee-chat",
  attributes: { slug: "coffee-chat", durationMinutes: 30, ... },
});

Standalone rows are used for:

  • Scheduling linkslookupKey: ":bookings:{slug}"
  • Provider data (Whoop, Garmin) — lookupKey: ":whoop:sleep", etc.
  • Forked builtin rows — user copies of system templates

The lookupKey serves as both a stable identifier and a protection mechanism: rows with a lookupKey are never automatically deleted.

Row Cleanup

When a sheet is hard-deleted, the system runs orphan cleanup:

DELETE FROM spreadsheet_rows
WHERE id NOT IN (SELECT row_id FROM spreadsheet_to_rows)
  AND lookup_key IS NULL
  AND (metadata->>'superseded')::boolean IS NOT TRUE;

This removes rows that are:

  1. Not linked to any sheet
  2. Have no lookupKey (not intentionally standalone)
  3. Not marked as superseded

Rows with a lookupKey always survive cleanup. Superseded rows (see below) are also preserved.

Superseding

When a standalone row is created with a lookupKey that already exists, the old row is superseded rather than blocking creation:

  1. The old row's lookupKey is saved in metadata.previousLookupKey
  2. The old row's lookupKey is cleared (freeing it for the new row)
  3. The old row is soft-deleted (deleted_at is set)
  4. The old row's metadata is tagged with superseded: true

The superseded row is preserved for investigation or recovery. It is not affected by orphan cleanup. The original lookupKey is always recoverable from metadata.

Old row: { id: "abc", lookupKey: ":bookings:chat", ... }
    ↓ supersede
Old row: { id: "abc", lookupKey: null, deleted_at: "...",
           metadata: { superseded: true, previousLookupKey: ":bookings:chat" } }
New row: { id: "xyz", lookupKey: ":bookings:chat", ... }

Soft Delete vs Hard Delete

Soft DeleteHard Delete
Mechanismdeleted_at = NOW()DELETE FROM
ReversibleYes (restore)No
WhenUnlink last referenceAdmin sheet delete
AffectsSingle rowOrphaned rows without lookupKey
lookupKeyClearedN/A (row removed)

Summary

  • Sheet-linked rows are cleaned up when their last sheet link is removed
  • Standalone rows (with lookupKey) persist indefinitely
  • Orphan cleanup only targets rows with no sheet link AND no lookupKey
  • Superseded rows are preserved even without a lookupKey
  • Always use a lookupKey for data that should outlive any particular sheet