Skip to content

Work reminders

How the work notes and reminders run, and how to check them. Rules: WRK-009 (the reminders), WRK-018 (a job given, a job finished) and COMM-041 (the phone push). What people see: Work inbox → Who is told, and when.

How it runs

  1. A job is given, handed on, finished or cancelled. Triggers on the work ledger (WorkItemEvent_notify_new_holder, WorkItemEvent_notify_giver) call work_tell(). It writes one AppNotification per person (kind work, marked "pushWanted") and queues the e-mail copy in CommunicationQueue (category work, message type work_notice). The web or the app that made the change then asks the communications-dispatcher to send now (its kick).
  2. Every 15 minutes the pg_cron job alhuda-work-reminders (*/15 * * * *) runs SELECT public.work_reminders_run(). It needs no secret: it only writes rows. For each open or in-progress work item due within the last 30 days it works out the working time left before the due time, or past it (less waiting), and fires the rungs that are due — due soon, due, 1.5×, 2×, +24 working hours — once per item, rung and due time (WorkReminder), through work_tell(). A rung crossed more than a working hour ago is marked done without a note (told = false): after an outage the first run does not flood anyone.
  3. Every five minutes the existing job alhuda-communications-dispatcher runs the communications-dispatcher. It puts back e-mail rows a stopped run left at processing for ten minutes (communication_queue_reap), sends the queued e-mails through the mailer, and then rings the marked notes of the last day through push-send (store:false), stamping "pushedAt" first — 8 seconds per push, 20 seconds in all per run. A push that failed is written in "pushError" and not tried again.

Checking

The job, in the SQL editor:

SELECT jobname, schedule, active FROM cron.job WHERE jobname = 'alhuda-work-reminders';
SELECT status, return_message, start_time FROM cron.job_run_details
 WHERE jobid = (SELECT jobid FROM cron.job WHERE jobname = 'alhuda-work-reminders')
 ORDER BY start_time DESC LIMIT 5;

What fired lately, and to whom (told = false: passed long ago, marked done without a note):

SELECT r."firedAt", r.rung, r.told, w.title, r."userIds", r."overdueMinutes"
  FROM "WorkReminder" r JOIN "WorkItem" w ON w.id = r."itemId"
 ORDER BY r."firedAt" DESC LIMIT 20;

E-mail rows put back after a stopped run:

SELECT "createdAt", "messageType", attempts, status, "lastError" FROM "CommunicationQueue"
 WHERE "lastError" LIKE 'The dispatcher run that took this message stopped%' ORDER BY "createdAt" DESC LIMIT 20;

Notes waiting to ring, and pushes that failed:

SELECT count(*) FROM "AppNotification" WHERE "pushWanted" AND "pushedAt" IS NULL;
SELECT "createdAt", "userId", title, "pushError" FROM "AppNotification"
 WHERE "pushError" IS NOT NULL ORDER BY "createdAt" DESC LIMIT 20;

A note marked for the phone with "pushedAt" set and no "pushError" was handed to push-send. If the person has no registered device, nothing rang, and that is not an error: the note is still in their notifications. The e-mail copies are in /communications → scheduled queue (category work).

By hand

A run now (the service role, or the SQL editor):

SELECT public.work_reminders_run();

It answers { now, checked, fired: { due_soon, due, lead, manager, leadership }, notes }. A second run at once fires nothing more. To see what a given moment would do, run it inside a transaction and roll back:

BEGIN;
SELECT public.work_reminders_run(timestamptz '2026-10-05 12:00+05:30');
ROLLBACK;

Stopping

  • The e-mail copies: Admin → Reminders → Automatic e-mails by category → Work off. The notes and the phone push still go.
  • The reminders themselves (only if they misbehave): SELECT cron.unschedule('alhuda-work-reminders');. Re-applying the migration schedules it again. A job given or finished still tells people.

Deploy

The migration 20261008150000_work_reminders.sql adds the columns, the table, the functions, the triggers and the job. Where pg_cron is missing it logs a notice and succeeds; run work_reminders_run() by hand then. Deploy with it:

  • communications-dispatcher — it now rings the marked notes (keptPush.ts);
  • push-send — it now ignores an old app build's own push for a work note, so a hand-over rings once;
  • mailer — the work_notice template.

Until the dispatcher is deployed, work notes still appear in the notifications and the e-mails still go, but no phone rings. The dispatcher's schedule needs its two Vault secrets (Deploy runbook → The dispatcher's schedule).