Changelog

0.5.0 — Automations goes native and runs for real, dialer buckets speak the view language, records notify the room

Automations lands as a first-class native app with a real runtime — webhook receiver, schedule dispatcher, DB-triggered event outbox, server-side executor, per-run history, and validation-gated publish. Dialer buckets stop carrying their own filter dialect and instead source their filters and priority from a linked saved table view. Record creation now toasts the org, the record sheet gains a Tasks tab with live count badges, and views become drag-to-reorder with filter/sort editing right in their settings.

What's new

Automations: native app + working runtime

Before this release, nothing on the platform ever created or executed an automation run — no scheduler, no webhook receiver, no event source, and the editor silently dropped node-config edits. 0.5.0 takes Automations from facade to working product.

  • Native app surface. Automations is now a kind: 'native' surface like Data / Pages / Calendar: elevated resizable left panel with search, status-dotted rows, confirm-gated delete, and a count footer, plus the flow editor embedded via EditorShell in pageMode. Registry / skeleton / contract tests per the house native-app pattern. See Automations → Layout.
  • Real triggers. DB triggers on records / tags / calendar feed an automation_events outbox (org-gated, change-gated); a per-minute matcher enqueues runs with 10-second dedup and a 30 runs/min per-automation circuit breaker. The schedule dispatcher walks next_run_at (interval or cron). The deployed automation-webhook edge function receives external POSTs on per-automation revocable tokens surfaced right on the trigger node. See Triggers.
  • Working manual path. The floating Run button in the editor executes the graph on the server (real writes, real timing). A Runs tab appears in the left panel for persisted automations, listing recent runs with per-node event timelines. Real run stats (last_run_at / last_run_status) flow back into the sidebar's status dots and relative-time labels. A pending-drain cron (migration 154) picks up any queued run the dispatchers may have missed. Fixed: the wait-node infinite loop, deep condition-branch pruning, executor auth + CORS, per-node progress persistence (no re-sends after crash). New run_once action supports agent-side delegation. See Runs tab.
  • One execution truth. The editor's Test button now runs a server dry run through the real executor — same code path as a live run, but sends and row writes are stubbed — replacing the divergent client-side simulator. Tag nodes write real tag_assignments; row nodes go through the atomic automation_patch_row_data RPC (migration 156, soft-delete semantics, schema-whitelisted fields); {{binding}} and {token} templating unify server-side; archived rows are excluded. See Test as a server dry run.
  • Trust & polish. Publish is validation-gated — no trigger, Schedule with no interval or cron, P88 Event with no source / table, orphan enabled nodes, or missing required fields (an SMS with no body, HTTP with no URL, …) surface as a checklist in the Logs panel and block the publish. The executor's pure core moved to core.ts with tests against the real module (replacing the copy-based test file); formula-engine string-literal operators are safe against overzealous rewrites, and >= / <= compile correctly. Service-role JWT auth via a verified role claim; a nightly retention cron (migration 157) trims run history. See Publish validation.
  • Editor truth. hasUnsavedChanges now fires for every edit path plus an unmount flush — a tab-close or navigation can't drop node config anymore. Transient test results no longer round-trip through the DB. The palette drops six broken nodes and exposes the real action nodes (send-sms, send-email, tag ops, row ops, HTTP).

Prod state: migrations 154 – 157 applied; edge functions deployed: automation-executor v11, automation-webhook v1. Verified end-to-end in prod (manual run through the deployed executor completed with correct condition evaluation, deep false-branch pruning as skipped audit events, per-node event log, run-stats updates; smoke-test data cleaned up). Everything is inert until an automation is published; the app itself is still IN_PROGRESS (dev-only) pending a UI pass.

Dialer buckets dial saved table views (one filter language)

Dial buckets now source their filters and dial priority from a linked saved view (dial_buckets.preset_id → filter_presets) instead of carrying a second, weaker, flat AND-only filter system. The bucket keeps only dial semantics: schedule, per-record cap, cooldown, field map, detail columns, eligibility.

  • Live link + session snapshot. The dialer snapshots the view (validated, relative date periods resolved to absolute instants) at session start and passes it to dialer_next_leads / dialer_queue_stats on every call — a mid-session view edit never yanks leads from under an operator. A banner offers Refresh queue when the underlying view changes.
  • One filter language. New app_hidden.filter_tree_predicate (migration 152) translates the view AST (AND / OR / NOT, full operator set) to SQL with rule-for-rule JS parity, replacing the dialer's parallel operator catalog and flat translator. child-link / formula / link rules are blocked at attach — the translator raises if they ever reach SQL. Verified by 35 golden parity fixtures spanning the deliberate quirk mirrors (case-insensitive equals vs case-sensitive is, JS Number() coercion, String(array) joins, epoch-0 null-date is_before).
  • Guardrails. Only org-scoped views can power a bucket — private views expose an inline Promote to org action in the picker. Deleting a view a bucket uses is refused (ON DELETE RESTRICT) with a clear message.
  • Bug fixed by construction. dialer_next_leads used to read filters off the bucket row, so a stored relative range ({period:"this_week"}) silently matched zero rows — the "QUOTED Last Week" bucket effectively wasn't a bucket. Filters now arrive pre-resolved from the client.

Data migration. All 7 existing buckets converted to org-scoped views and linked after confirming 0 membership diffs old-vs-new per bucket; re-verified against prod post-conversion (Quoted 343, Guillotine 40, YesToQuote 54, NextGen 50, Ghosted 68, NEW 62). Migrations 151 – 153 applied. The RPCs still fall back to the legacy flat bucket filters when a client doesn't pass a snapshot, so pre-deploy clients keep working. See Dial → View-driven filters and sort.

Also rolled in — tag-assignment fetch fix. Client-side tag-filtered views and counts silently undercounted: the grid warmed tag assignments for filtering with one bulk listAssignments call across every loaded row, but PostgREST caps un-ranged selects at 1,000 rows and a ~900-uuid .in() list can also exceed URL length limits. Rows past the cap looked untagged to the filter engine while their per-row chips (fetched individually) still rendered — so a view appeared to match far fewer rows than the same filters in a bucket. listAssignments now fans out in 100-id chunks and paginates each chunk, preserving the per-entity oldest-first ordering contract. Server-side dial queues were never affected.

Record creation notifications

Every record creation now toasts — for the creator immediately, and for every teammate in the org via realtime — each with a click-through that opens the record.

  • Creator side. After addRow / addRowWithChildren, a success toast with an Open record action. CSV imports and bulk conversions get one aggregate toast / notification instead of N.
  • Teammate side. Creation also inserts an org-scoped row in the existing notifications table (category records); everyone else in the org receives the same toast via the notifications realtime channel. The bell popover gets a clickable history row with a Database icon.
  • Deep link. New RecordDeepLinkHost consumes /:orgSlug/data?table=<slug>&row=<id> and opens RecordDetailSheet. The command palette's record results already generated this URL via routeFor('user_table_row') — palette records now open the sheet cleanly.
  • Guardrails. No creator echo (metadata.created_by compare); 10-minute staleness window silences reconnect backfill (mirrors the Gmail toast guard); import / conversion aggregate into single notifications; a new Records notification-preference category (default on) gates teammate toasts; stale deep links clear themselves instead of dead-ending.

No migration — the notifications table already had org-member INSERT RLS and realtime publication membership. See Data → Toast notifications on record creation.

Record detail sheet: Tasks tab + live count badges

  • Tasks tab. Tab order is now Details → Events → Tasks → Activity. The new Tasks tab reuses the dialer's per-record task panel (DialTasksTab) with record-flavored wording so both surfaces share one composer, cache, and task-bus sync. Toggling a task syncs the top-bar Tasks pill, the org-wide Tasks widget, and the calendar's task chips.
  • Live count badges. Events, Tasks, and Activity each show a live count badge in the tab bar — fetched up-front as an exact server-side head count via count: 'exact', head: true queries, so the numbers show before a tab is ever opened. Refresh on task_changed / row_bumped / activity_removed scoped to the row. Badges hide at zero.
  • activity_removed bus channel — deletes don't fire the monotonic last_activity_at bump (that pinned contract is preserved and still tested), but badge consumers need to know the activity set shrank.

See Data → Tabs and live count badges.

Data views: drag-reorder + filter/sort editing in view settings

  • Views drag-reorder like tables. Saved-view rows in the sidebar tree are draggable with the same styling as table rows, on their own MIME type so table and view drags can't cross-react. System rows (All records, Archived, Create view) are fixed. Order flows into the grid header's view dropdown too, since both read the same preset cache. Migration 154_filter_presets_sort_order.sql adds the column and backfills the existing 16 presets in current display order so nothing visibly shuffles.
    • Persistence goes through a narrow reorder_filter_presets SECURITY DEFINER RPC (RLS only lets a view's owner UPDATE it, but a drag must move teammates' org-shared views) — the RPC writes sort_order only and is gated on org membership plus per-row SELECT-equivalent visibility.
  • Filters + Sort in view settings. The gear-opened ViewSettingsSheet gains Filters and Sort sections using the same FilterTreeEditor / SortListEditor the table settings already use. Editors are seeded through applyPresetToWidget so rules on deleted columns drop at open time; untouched sections aren't written back on save.
  • Active view re-applies on external edit. useTableViews re-applies the ACTIVE view when its saved definition changes externally (e.g. edited via the sheet, or a teammate re-scopes the org-shared view). Dirty grids always win. Content compare strips node ids since legacy flat presets regenerate ids on normalize and would otherwise loop the effect.

See Data → Saved views.

Under the hood

  • app_hidden.filter_tree_predicate (migration 152) — the single SQL translator of the view AST. Injection-safe (format(%L)), parity-tested against applyTableFilters, and blocks cross-table rule types before they reach SQL.
  • reorder_filter_presets — SECURITY DEFINER RPC that stamps filter_presets.sort_order on org-shared and private views alike, gated on membership + per-row SELECT visibility.
  • automation_events (migration 155) — service-role outbox fed by DB triggers on records / tags / calendar; drained by the per-minute event matcher.
  • automation_patch_row_data (migration 156) — atomic RPC the Update Row node writes through; soft-delete semantics and a field whitelist keep automations honest about the data contract.
  • Retention cron (migration 157) — nightly trim of automation_runs / automation_run_events so the Runs tab stays scannable.
  • shared/tableSortFilterEditors.jsxSortListEditor, TableFilterValueInput, and makeSortId moved out of DataTableViewerWidget.jsx to break the grid → ViewSelect → ViewSettingsSheet → grid import cycle (the grid re-exports for back-compat).
  • useRecordTabCounts — one place to fetch and refresh the record sheet's three tab counts; api.calendarEventLinks.countByRow and api.recordActivities.countByRow are new head: true count queries.
  • RecordDeepLinkHost + routeFor('user_table_row') — a standalone host component that opens RecordDetailSheet from the /:org/data?table=&row= URL, wired to the notification toast's Open action and the command palette's record results.

What's next

  • UI pass on Automations so the app can graduate out of IN_PROGRESS and become visible in prod.
  • Drop the legacy flat-filter fallback on dialer_next_leads / dialer_queue_stats and remove dial_buckets.filters / dial_buckets.sorts once no clients still hit it.
  • Per-node retry policy for the automation executor, and an HTTP-auth secrets store so headers stop living in pipeline JSON.
  • Encrypted / sensitive field support for the automation executor (invisible to it today).

See Automations, Triggers, Runs tab, Dial → View-driven filters and sort, Data → Saved views, Data → Tabs and live count badges, and Data → Toast notifications on record creation.

On this page