The robot that chases people: automated reminders, stale quotes and follow-ups

by

in artificial-intelligence,business-operations,

August 24, 2026

Most operations teams have a version of the same list. Quotes sent three weeks ago with no answer. A job waiting on a client document since before the last public holiday. A renewal that needed a conversation in June and is now awkward. Nobody decided to drop these. They fell through a crack, and nobody was paid to watch the cracks.

The usual fix is a person: an ops coordinator with a spreadsheet and a Monday morning ritual. That works until they take leave. The better fix is a scheduled task with an opinionated data model behind it, plus a language model doing the one part that genuinely needs judgment, which is writing a chase message that does not read like it came from a machine.

This is part of our Practical AI in Business Operations series, and it is deliberately the least glamorous entry. There is very little AI in it. That is the point: the value comes from the scheduler, and the model earns its place at the last step.

The table is most of the trick

A working reminder system is smaller than people expect. An open design discussion on a tender tracking project sketches a reminders table keyed on the record id plus days_before, sent and sent_at, swept by a daily cron. The state columns are what stop the same nag firing twice. That is the whole mechanism.

Ours look roughly like this:

create table reminders (
  id                uuid primary key,
  entity_type       text not null,          -- quote | job | invoice | renewal
  entity_id         uuid not null,
  milestone         text not null,          -- first_follow_up | stale_quote | decision_chase
  days_before       int,                    -- exactly one of these two is set
  remind_on         date,
  due_date          date not null,          -- computed; the only thing the sweep looks at
  assignee_id       uuid not null,
  snoozed_until     date,
  suppressed_reason text,                   -- an honest reason to stop chasing
  sent              boolean not null default false,
  sent_at           timestamptz
);

The sweep is one query, run daily:

select * from reminders
where due_date <= current_date
  and not sent
  and (snoozed_until is null or snoozed_until <= current_date)
  and suppressed_reason is null;

Note due_date <= current_date, not = current_date. Exact date equality means one bad deploy day silently drops every reminder that fell on it. The same ticket works through that and two related traps, and lands on catch-up rather than skip, with reminders whose milestone has already passed suppressed in favour of a louder “this was missed” state. It also resets sent when a deadline moves: a write-once boolean means pushing a due date back permanently silences the record.

Timezones matter more here than they look. An organisation with staff in Perth and Sydney has a two or three hour window where “today” is two different dates. Pick an org-level timezone, compute due_date in it, and stop treating server time as the answer.

Deciding when it fires is the same decision as deciding how someone hears about it

The tender ticket treats notification delivery and the reminder model as one design question, not two, and that is right. A reminder that broadcasts to a shared channel has different semantics from one that reaches an individual. If your only channel posts to a group, then every reminder is public, and that constrains the content: no pricing, no margin, and a hard look at whether client names belong in a channel that may include contractors. Under the Australian Privacy Principles, “who can see this notification” is a design input, not a compliance review item at the end.

Targeting also decides whether the system survives. Reminders that ping people who have already done the work train the whole team to mute the robot within weeks. Chase the assignees with nothing recorded against the milestone, not everyone attached to the record.

Escalation is a schedule, not a mood

Escalation should be boring and configurable. For a client submission deadline, defaults of seven days, three days, one day and the morning of the due date work well. Two rules make it useful rather than noisy:

  • A missed item does not drop off the list. It stays on the dashboard in a loud state until someone resolves it, and it posts once to the team channel when it is missed.
  • Escalation changes audience, not just frequency. The third reminder goes to the assignee’s manager. If your escalation only shouts louder at the same person, it is not escalation.

For stale quotes, the trigger is different but the shape is the same: days since sent with no client response, days until the quote’s validity expires, and a hard stop at 30 June for anything priced against a financial year.

Snooze, and the re-arm problem

Snooze is where these systems go wrong, and the failure is not the trigger. It is when the system decides to re-arm after a human acts.

There is a neat illustration of this from an unrelated field. A robot’s heading-hold controller was counter-steering after a manual turn: the operator would turn, recentre, and the robot would swing back the other way. The hold released during the turn and re-latched at the wrong point, against a target the machine had already rotated past. The fix was to shorten the hold’s memory so the target caught up with reality faster.

Reminders have the identical bug. Someone rings the client on Tuesday and snoozes for a week. On Wednesday the client replies and the quote status changes. If the snooze re-arms against the old due_date, the reminder fires next Tuesday about a conversation that is already finished, and the person who did the right thing gets nagged for it. Recompute due_date on every state change to the underlying record, and let the snooze expire against the recomputed value. The rule is simple: the reminder chases the current state of the record, never the state it had when the row was written.

Give people an honest way to stop the nag

The most valuable column in that table is suppressed_reason. Without it, “hasn’t got to it” and “this cannot be actioned” look identical to the scheduler, and it chases people forever for work that is impossible. The tender ticket adds an explicit “no supplier found” record for exactly this reason. Ours are things like “client asked us to hold until the new financial year” or “waiting on their legal, no date”. Each one silences the reminder and leaves a written trace of why, which is far more useful in a Monday meeting than a snoozed row.

Where the model actually goes

Two places, both narrow.

The first is classification. A rule that decides whether a quote is stale by looking at a status field alone will get it wrong, because status fields lag reality. Reading the actual email thread and the file notes is a better signal, and that is a job a model does well. One caution: read the whole record, not a sample. A scheduled job in another project fetched a partial version of its source text for years, and the correction was explicit, to pull all of the text rather than a sample of it. Reminder rules that peek at only the last two emails will confidently call a dead quote fresh.

The second is drafting the chase message. The model gets the client name, what was sent, when, what has happened since, and two or three examples of how that account manager actually writes. It produces a draft. A person reads it, changes a line, and sends it from their own mailbox.

We do not auto-send these, for two reasons. Clients can tell, and an unsupervised bot emailing your customers about commercial matters is a Spam Act 2003 problem waiting to happen rather than a productivity gain. The point of the automation was never to write the email. It was to make sure someone was asked to.

Design every state before you build

Alert preferences deserve real screens, not a config file. One mobile build we looked at gives alerts a dedicated /settings/alerts route alongside profile and saved items, and enumerates every screen at every lifecycle state for review. Copy that discipline. Pending, due, sent, snoozed, escalated, missed, suppressed and dismissed are eight states somebody has to design, and the ones teams skip are always suppressed and missed, which are the two that decide whether people trust the thing.

Costs and honest limits

The model calls are the cheapest part. A few hundred short drafts a month is a rounding error next to the engineering time, and most of that time goes into failure semantics rather than prompts. Budget for a small build with a fortnight of tuning the thresholds afterwards, because the first set of defaults will be wrong.

The real limit is consumption. Scheduled automation whose output nobody acts on is worse than none, because it costs attention and buys nothing. That same project found an extraction job that had run against roughly twelve articles a year, was compared against a model’s reading, “and then used for nothing else”. The author’s verdict was blunt: that is backwards. Before you build the chaser, decide who reads its output and what they do next. If the answer is thin, build the daily digest instead and stop there.

PicNet builds production AI systems for Australian organisations. Talk to us about what a first project could look like.

Tagged: #automation #workflow-automation #crm #scheduled-jobs #llm