Core Concepts

Records

A record is a row in a data table — but the detail UI treats it as a first-class entity with tabs for events, activity, notes, and child records.

A record is a single row in a user data table. The underlying storage is just a user_table_rows row, but the UI promotes records to first-class status: clicking a row opens the Record Detail Sheet, a rich right-side drawer with tabs for events, activity, notes, and linked / child records.

The Record Detail Sheet

src/modals/RecordDetailSheet.jsx is a right-side drawer with two modes:

  • View mode — read-only field display via ViewModeBody.
  • Edit mode — buffered editing. Field changes accumulate in an editBuffer and only commit on Save Changes, so partial edits never hit the database.

The header always shows:

  • The display title (the row's primary label column)
  • The row's tags (always editable via <TagPicker entityType="user_table_row" />)
  • Save / Edit / Close buttons

Then four tabs:

Details

A two-column grid of the row's fields, plus inline sections for linked records and child records and read-only system columns (created_at, updated_at, last_activity_at).

Events

Calendar events linked to this record via the calendar_event_links join table. Each row in calendar_event_links joins (event_id, calendar_id) to (table_id, row_id). The tab lists linked Google Calendar events with title, date, time, and location, and a + New event button creates a new event already linked.

Activity

A polymorphic timeline rendered by RecordActivityTab. Activities live in the record_activities table with a kind discriminator — call, email, meeting, sms, task, note, other. The Dial mode writes call activities here, the Inbox writes email activities, etc.

Notes

A focused note composer (Cmd-Enter to submit). Notes are stored as kind = 'note' rows in record_activities, so they share the same timeline as everything else but get their own tab for quick capture.

last_activity_at

Every row has a last_activity_at column managed by a Postgres trigger (migration 064_last_activity_at.sql). It bumps — never backwards — on:

  • INSERT to record_activities for that row.
  • INSERT to tag_assignments with entity_type = 'user_table_row' for that row.

An index on user_table_rows(table_id, last_activity_at DESC) makes "records sorted by recency" cheap. The viewer can surface this column; filter presets can target it.

Child-record navigation

Records can have child records in other tables, declared via child-link columns. The Detail Sheet shows each child group inline (table name, row count, + Add button) and clicking a child card opens that child's own Detail Sheet — with the parent's sheet sliding back. The onBack prop on RecordDetailSheet powers this nested navigation.

The discovery logic (findIncomingLinkedRows in src/modals/recordEditor/childTableDiscovery.js) scans every table in the workspace for columns that link back to this row.

Bulk convert

ConvertRecordsModal (src/modals/convertRecords/ConvertRecordsModal.jsx) maps records from one table to another. Three tabs:

  1. Map Fields — choose how source columns map to target columns. autoMap() uses label / key heuristics to seed the mapping.
  2. Optionsinherit_tags (default on) carries record tags across; delete_source removes source rows after a successful convert.
  3. Preview & Run — preview the first N rows, then run.

Capped at 1000 rows per run. Backed by RPCs in migration 056_record_conversion_rpcs.sql with per-batch tracking.

Where to next

On this page