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 viaEditorShellinpageMode. Registry / skeleton / contract tests per the house native-app pattern. See Automations → Layout. - Real triggers. DB triggers on records / tags / calendar feed
an
automation_eventsoutbox (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 walksnext_run_at(interval or cron). The deployedautomation-webhookedge 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). Newrun_onceaction 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 atomicautomation_patch_row_dataRPC (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.tswith 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.
hasUnsavedChangesnow 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_statson 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/linkrules are blocked at attach — the translator raises if they ever reach SQL. Verified by 35 golden parity fixtures spanning the deliberate quirk mirrors (case-insensitiveequalsvs case-sensitiveis, JSNumber()coercion,String(array)joins, epoch-0 null-dateis_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_leadsused 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
notificationstable (categoryrecords); 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
RecordDeepLinkHostconsumes/:orgSlug/data?table=<slug>&row=<id>and opensRecordDetailSheet. The command palette's record results already generated this URL viarouteFor('user_table_row')— palette records now open the sheet cleanly. - Guardrails. No creator echo (
metadata.created_bycompare); 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: truequeries, so the numbers show before a tab is ever opened. Refresh ontask_changed/row_bumped/activity_removedscoped to the row. Badges hide at zero. activity_removedbus channel — deletes don't fire the monotoniclast_activity_atbump (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.sqladds the column and backfills the existing 16 presets in current display order so nothing visibly shuffles.- Persistence goes through a narrow
reorder_filter_presetsSECURITY DEFINER RPC (RLS only lets a view's owner UPDATE it, but a drag must move teammates' org-shared views) — the RPC writessort_orderonly and is gated on org membership plus per-row SELECT-equivalent visibility.
- Persistence goes through a narrow
- Filters + Sort in view settings. The gear-opened
ViewSettingsSheetgains Filters and Sort sections using the sameFilterTreeEditor/SortListEditorthe table settings already use. Editors are seeded throughapplyPresetToWidgetso rules on deleted columns drop at open time; untouched sections aren't written back on save. - Active view re-applies on external edit.
useTableViewsre-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 againstapplyTableFilters, and blocks cross-table rule types before they reach SQL.reorder_filter_presets— SECURITY DEFINER RPC that stampsfilter_presets.sort_orderon 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_eventsso the Runs tab stays scannable. shared/tableSortFilterEditors.jsx—SortListEditor,TableFilterValueInput, andmakeSortIdmoved out ofDataTableViewerWidget.jsxto 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.countByRowandapi.recordActivities.countByRoware newhead: truecount queries.RecordDeepLinkHost+routeFor('user_table_row')— a standalone host component that opensRecordDetailSheetfrom 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_PROGRESSand become visible in prod. - Drop the legacy flat-filter fallback on
dialer_next_leads/dialer_queue_statsand removedial_buckets.filters/dial_buckets.sortsonce 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.
0.5.1 — Create view is a modal with a live record preview
Creating a saved view now opens a proper modal with name, sharing, filters, and sort in one place, plus a live record preview that shows exactly which rows the view will match — before it exists.
0.4.0 — Real-time Calendar & Inbox, Data views tree + settings gears, top-bar Create
Google Calendar and Gmail push notifications land — bookings and new mail toast on every app surface. The Data sidebar gains a full views tree with record counts and per-table / per-view settings gears. Every surface now carries a global Create button and a click-only profile menu. Dial gets a per-lead Tasks tab, and calendar tasks render with their own styling.