The events your app can't emit
Your application emits events when its code runs. It can't announce what didn't happen. Here is how a SQL trigger catches absences and derived conditions.
- PostgreSQL
- Slack
- Zendesk
- Braze
- concepts
- sql
Most product analytics and messaging tools start from the same assumption:
the application tells them what happened. A user signs up, the code calls
track("signed_up"), and everything downstream reacts.
That works for events that are a line of code. It fails for three kinds of event that teams ask for all the time.
1. Absences
A trial ends in three days and the account never activated. An urgent ticket has had no first response. A customer hasn’t logged in for 14 days.
Nothing happens at the moment these become true. No request comes in and no handler runs, so there is nowhere to put the tracking call. The usual workaround is a cron job someone has to write, deploy and own.
As a query it is just a WHERE clause:
SELECT a.id AS account_id, a.name AS account, a.owner_email,
a.trial_ends_at - interval '3 days' AS nudge_at
FROM accounts a
JOIN plans p ON p.id = a.plan_id
WHERE p.name = 'Trial'
AND a.activated_at IS NULL
The clock doesn’t even have to be a column: here it is three days before the trial ends, computed in the query.
2. Derived conditions
The third declined charge this month. An account past 80% of its seats. Each one is a count, a threshold or a join across tables. Application code could compute them, but only if someone adds that computation to every code path that changes the underlying rows.
The database already holds the answer. A query asks it.
3. Data that is already there
Even when an event could be emitted by the app, adding it means a ticket, a code change and a release. Then it only fires for activity after the release. If the data is in the database today, a trigger on it works today, and a backtest shows what it would have done over the last 120 days before anything is sent.
Why not a cron job
A scheduled query that posts its rows is a few lines of code. Running it for months without spamming anyone is the hard part, and it comes down to four problems:
- The query keeps matching. A trial that never activated is still unactivated on the next run, and the one after. Something has to remember what already fired, per key, and how often it may fire again.
- The first run sees all of history. Switch on “no login for 14 days” and every dormant account from the last three years qualifies at once.
- Rows arrive late. A row committed a few seconds after a poll can carry
an
event_timefrom before it. A window that starts exactly where the last one ended misses it. - Polls overlap. Two workers, or one slow poll and the next, see the same row. Check-then-send lets both send.
Rowfire’s answers, in the same order. Each rule has a key and a cadence,
and every send is a row in a ledger with a unique index on (rule, key,
period). A new trigger’s watermark starts at now, so switching it on
fires nothing from the past. Each poll re-scans a short lookback behind the
watermark, which is safe because the ledger already holds what fired:
re-polling the demo’s 120 days a second time matches 697 rows and fires
none. And a send happens only if the ledger INSERT reports it inserted a
row, which is the subject of
sixteen workers, one send.
The computed clock in the first query works because of the window. Rowfire
wraps the query and keeps rows whose event_time falls between the last
poll and now. nudge_at is in the future until three days before the
trial ends, so the row enters the window once, when that moment passes.
That’s how an absence becomes an event.
One query, many subscribers
In Rowfire the query is a trigger: written once by whoever knows the schema. Teams subscribe to it with rules that decide how often one key may fire (once ever, once per day or week, every nth time) and what happens: a Slack message, a Zendesk ticket, a Braze event or a call to any API.
In the live demo, one payment_failed trigger feeds two rules: a #billing
heads-up at most once a day per account, and a Zendesk ticket at most once a
week. Over 120 days of sample data, 211 declined charges become 105 tickets.
The other 106 were retries that would have been duplicates.
See it on the use case page, or try it in the demo.
Questions
Why not add a tracking call to the application?
For an event that a line of code produces, you can. For an absence (a trial that never activated) or a derived condition (the third declined charge this month), no line of code runs at the moment it becomes true, so there is nothing to put the call in.
How quickly does Rowfire notice?
Triggers are polled, within a minute by default. That suits tickets, campaigns and follow-ups, not messages that must arrive instantly, like one-time passcodes.