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-30drop shadow over the grid keeps the rail visually layered. - Top bar — grid toolbar — the active table's
ViewSelectdropdown, 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
ViewSelectdropdown 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.
- The table's name doubles as the
- 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.stringifydiff of filter tree + sorts vs. snapshot. - Sorts are part of the view. Migration
115adds asortscolumn tofilter_presets;presetSchemavalidates the new shape andapplyPresetToWidgetdrops any sort whose field went missing. - Cross-channel sync. Picking a preset from inside the Filter
dropdown also updates the title's active view —
FilterMenuWithPresetsemitsonPresetApplied / onPresetSaved / onPresetDeletedso 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 left —
Clear all. Inside a table widget, clearing routes throughuseTableViews.clearView, so the title'sViewSelectand 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.
Header search
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_tagscolumn. Off hides the synthetic_tagscolumn in the data-table viewer while keeping existing assignments intact. Useful for tables likeclientswhere 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
formatis unset. Resolution order iscol.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_titletags(text array)source,status,opted_in_sms,opted_in_emailcity,state,countrylast_contacted_at,conversation_countcustom_fieldsJSONB 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
Pages
The Notion-style document mode — unified Pages workspace widget with tree, folders, and block editor side-by-side.
Calendar
Project88's universal calendar — Google dual-write, a default Project 88 calendar that mirrors every internal write, shareable calendars with on-behalf-of booking and visibility tiers, reminders, record linking, and tasks on the schedule.