Apps

Data

The Airtable-style data mode — typed tables, inline editing, and the native People table.

Data is the structured-data mode. It shows your tables flat with inline editing, sorting, and filtering.

Data opens as a native shell surface — full-bleed, no React Flow canvas underneath. See Canvases and widgets → native apps vs canvases. Dashboard-style data widgets (DataTableWidget, DataTableListWidget, DataTableViewerWidget, DataRecordWidget, DataStatsWidget) remain addable to Home and any board for embedded views.

Layout

DataWorkspace combines what used to be two separate canvas widgets — the table list and the grid — into one master / detail surface mounted by DataSurface (which wraps a ReactFlowProvider so the grid's useReactFlow hooks keep working). On the native surface it runs as the shared two-column shape Calendar and Dial use: a full-height table-list sidebar on the left, with the grid's toolbar riding the chrome's darker top bar over the inset white card on the right (the shared NativePaneChrome):

  • Left rail (full-height) — elevated list of every table in the org with icon, row count, and SYS badge for system tables; click any row to switch. The "Data" title + New table button live in the top of the rail (not on the chrome top bar), so the rail reads as the app's primary nav. A z-30 drop shadow over the grid keeps the rail visually layered.
  • Top bar — grid toolbar — the active table's ViewSelect dropdown, header search input, and the right-side action set (filter, sort, import, insert) all ride the darker top bar over the right column.
    • The table's name doubles as the ViewSelect dropdown trigger, modeled on the dial bucket selector. The dropdown lists All records plus every saved view; hover any view row to reveal inline rename / delete actions.
    • The header search input is borderless until focus.
    • Filter and sort dropdowns right-align to their trigger; Import is an IconButton.
    • Because the rail already drives the active table, the older in-grid table switcher is hidden inside the workspace.
  • Table grid — the active table rendered flat in the inset white card. Click any cell to edit.

The legacy DataTableListWidget / DataTableViewerWidget node types still render on canvases that already use them — DataTableListBody is shared between the rail and the standalone widget so layout stays in sync. The full-height-sidebar arrangement is the native opt-in (nativeLayout); the canvas widget keeps a single-column chrome with Data + New on the card's top bar and the grid's toolbar inside the right pane.

Saved views

A view is a named snapshot of the data-table's filter tree and sort priority, saved against the table. Pick one from the title's ViewSelect and the grid filters + sort populate; the title updates to the view name.

  • Create / rename / delete inline. The dropdown has an inline create form; hovering a view row reveals pencil + trash buttons. Renaming the active view updates the title in place; deleting it flips the title back to All records.
  • Dirty-state indicator. When the live filters/sort diverge from the saved view's snapshot, an unsaved dot appears next to the title and the dropdown gains a "Save changes to ‹view›" footer. Click it and the dot clears. Dirty detection is a JSON.stringify diff of filter tree + sorts vs. snapshot.
  • Sorts are part of the view. Migration 115 adds a sorts column to filter_presets; presetSchema validates the new shape and applyPresetToWidget drops any sort whose field went missing.
  • Cross-channel sync. Picking a preset from inside the Filter dropdown also updates the title's active view — FilterMenuWithPresets emits onPresetApplied / onPresetSaved / onPresetDeleted so the two surfaces stay in sync without a second source of truth.
  • Scope. Workspace scope was dropped from the user-facing surface; the scope picker is now a private / org toggle.

Internally, views are still filter_presets rows (useTableViews is a shared hook so Medium + Large widgets stay in sync). The active view id lives in component useState only — it's not persisted across reloads.

Archive and the Archived view

The bulk-action toolbar's destructive button is Archive rather than Delete. Selected rows pick up an archived_at timestamp and drop out of the active view but remain in the database.

ARCHIVED_SYSTEM_VIEW is always appended to every table's view list — pinned at the bottom of the picker below a divider with the Archive icon. It can't be renamed, deleted, or saved over; user filters and sorts are ignored inside it. Inside the Archived view, the bulk-action set switches to Export / Restore / Permanently delete — the last gated by a ConfirmDialog. Per-row affordances on RecordDetailSheet, DataPage, and DataWorkspace rename Delete to Archive with the same two-click confirm pattern.

The shared getBulkActions(view) helper returns the action list per view kind, so both the Medium and Large DataTableViewerWidget toolbars stay in lockstep. See Data tables → Archive vs. delete for the storage shape.

Range operator on date filters

Date and datetime columns expose an in period (range) operator in the filter menu with five built-in choices:

  • This week / Last week — boundaries follow the user's Week start day preference.
  • This month / Last month — calendar-month boundaries.
  • Custom range — expands two inline DatePickers for an explicit from → to window (end day inclusive).

Values are stored as objects: { period: 'this_week' } for named periods, { period: 'custom', start, end } (YYYY-MM-DD) for custom. Boundary math lives in src/lib/dateRanges.js — pure and DST-safe — and applyTableFilters precomputes the resolved window once per pass via precomputePeriodRanges so the boundary is stable across all rows. The same primitive backs the dialer bucket builder's date filters.

Filter menu layout

The filter menu's chrome is laid out for two-handed editing of presets

  • rules in one place:
  • Header right — the preset selector dropdown. The trigger shows the active preset's name (falls back to Apply preset… when no preset is active).
  • Footer leftClear all. Inside a table widget, clearing routes through useTableViews.clearView, so the title's ViewSelect and the filter chips clear in lockstep with the underlying tree.
  • Footer right — primary Save button. Disabled until the current tree diverges from the snapshot that was last applied, re-enabled the moment any rule, group, or sort changes.

Picking a preset from inside the filter dropdown emits onPresetApplied / onPresetSaved / onPresetDeleted, which keeps the title's active view and the filter menu in sync without a second source of truth. The custom-range date picker inside the menu now renders into the popover layer (data-popover-layer) so picking a day doesn't collapse the menu.

Both the data-table title row and the calendar week widget header now host a borderless-until-focus search input. The data-table search matches any column, and phone-typed columns match with formatting stripped — 3176449695 or 317-644 will find a stored (317) 644-9695. Same lib/searchMatch.js helper used by the dialer header search.

Creating a table

Click the breadcrumb dropdown → + New table. The NewTableModal asks for:

  • Name and slug
  • Lucide icon
  • Initial columns

Columns can be added later via the toolbar.

Per-table settings

EditTableSheet's Settings tab is laid out as collapsible Section blocks. Two recent additions:

  • Enable tags — flips the use_tags column. Off hides the synthetic _tags column in the data-table viewer while keeping existing assignments intact. Useful for tables like clients where tagging isn't part of the workflow.
  • Default formats — per-type presets (date, datetime, time, number, price, percent, phone, email) used as a fallback when a column's own format is unset. Resolution order is col.format → table default → hardcoded default. The resolver is threaded through every render path (data-table viewer rows, the record detail sheet, the record editor, child-link rollups, and the date / phone pickers in edit mode), so the value you see while editing matches the value you see while reading.

Both settings default to behavior-preserving values — existing tables aren't affected until you toggle them.

Column types

The full registry lives in src/modals/recordEditor/colTypes.jsx. There are 15 column types — see Data tables for the full table. The short list:

text, number, phone, email, price, percent, date, datetime, time, boolean, select, multi-select, link, child-link, formula.

Inline editing

Click any cell to edit. Updates are optimistic — the cell updates immediately, then persists. On error, the cell rolls back and a toast explains.

Drag a row's grip to reorder. Save status indicators ("Saving..." / "Saved" / "Save failed") appear in the toolbar.

The People table

Your org's first table is People, created automatically via the ensure_people_user_table() SQL function. It has typed Postgres columns optimized for scale (100k+ rows):

  • first_name, last_name, email, phone, company, job_title
  • tags (text array)
  • source, status, opted_in_sms, opted_in_email
  • city, state, country
  • last_contacted_at, conversation_count
  • custom_fields JSONB for user-defined columns

The Data mode shows it like any other table, but with a SYS badge. The SMS mode and Campaigns mode read from it directly.

Tables as agent context

Agents can query your tables with the built-in search_user_tables tool. That's how you give an agent access to your CRM, ticket queue, or product catalog without writing any glue code.

Where to go next

On this page