← Engineering

· 8 min read · Michael Archer

How we built automated appointment reminder texts for rental repairs

When I wrote about the four gates Doot stops at, I left one question open: what happens when someone just stops replying? A tenant who never answers the availability text, or a contractor who reads "can you still make Thursday at 2?" and goes quiet, stalls the whole job.

This post covers what we shipped to fix that: automated appointment reminder texts before every booked repair, follow-up texts when someone goes quiet, and an email to the landlord when a job stays stuck. Most of the work wasn't sending the texts. It was deciding when not to send them.

Who gets reminded, and when

ReminderWho gets itWhenChannel
Appointment reminderTenant, and the contractor if they're reached by text24 hours and 3 hours before the visitText
Morning-of reminderTenant and landlord, when the landlord is doing the repairFrom 7am on the dayText
Follow-up when someone goes quietWhoever Doot is waiting onEvery 6, 12, 24 or 48 hours, set per jobText
"Still waiting" noticeLandlordEvery 24 hours a job stays stuckEmail

Pausing a job or marking it resolved stops all of them.

What the appointment reminder texts say

A reminder only helps if it answers the questions people actually have: who's coming, when, and do I need to be home. Here's what a tenant gets 24 hours before a visit:

Doot: reminder -- Mike's Plumbing is coming tomorrow at 2:00 PM for "Kitchen sink leak". Please make sure someone's home to let them in. Let me know if you'd like me to reschedule.

And the contractor, on the same booking:

Doot: reminder -- you're booked tomorrow at 2:00 PM at 14 Elm St (Unit 2) for: kitchen sink leaking under the cabinet. The tenant will be home to let you in. Let me know if you'd like me to reschedule.

The entry line comes from a question Doot asks the tenant up front: can the repair person let themselves in? If the tenant said yes, the tenant's text says "They'll let themselves in, since you said that's OK" and the contractor's says "The tenant said it's OK to let yourself in." That one line prevents the most common wasted trip, which is a contractor at a locked door.

Both texts end with an offer to reschedule, and that matters more than it looks. A booked job is still a live conversation, so a reply like "can we move it to Friday?" goes straight to Doot's reply handling and starts a reschedule. A new time gets its own fresh set of reminders.

Follow-up texts when someone goes quiet

While a job is waiting on a reply, such as the contractor's available times, a tenant confirming a slot, or "is the repair done?", Doot re-texts the person it's waiting on. Each follow-up repeats the actual question rather than a generic nudge:

Hi Mike, Doot again -- following up on the repair at 14 Elm St: kitchen sink leaking under the cabinet. Are you able to take it? If so, what are 2-3 days and times that work for you?

The landlord sets the pace on each job's page: every 6, 12, 24 or 48 hours, or off. New jobs default to 24 hours. The page also shows how many follow-ups have gone out and roughly when the next one is due, so the landlord never has to guess what Doot is doing.

The rules for when a follow-up goes out are where the design lives:

  • The clock restarts on every new question. When Doot asks something new, the follow-up count resets and the interval starts over.
  • Any reply pushes the next follow-up out. If a contractor texts "I'll get back to you shortly," that doesn't answer the question, but sending a reminder ten minutes later would just be nagging. Any text from the person Doot is waiting on pushes the next follow-up a full interval out.
  • Phone-only contractors get no follow-up texts. If a contractor is set to be reached by phone, Doot doesn't start texting them. The landlord's email covers it instead.

The "still waiting" email to the landlord

Follow-ups handle the other person. The landlord needs to know too, without being pinged every time Doot sends a nudge. So for every 24 hours a job stays stuck on the same question, the landlord gets one email: who Doot is waiting on, what it asked, how long ago, and how many follow-ups it has sent. The email also says plainly that nothing is required. Doot keeps following up, and the landlord can call the person or pick someone else if they'd rather not wait.

This clock deliberately doesn't reset when the quiet person sends a non-answer. "I'll get back to you" pushes back the next text to them, but the job still hasn't moved, so the landlord's daily count keeps running.

Quiet hours, in the landlord's timezone

Nobody wants a "your plumber is coming at 8am" text at 5am. Every reminder has a sending window:

  • Appointment reminders go out between 7am and 9pm. A 3-hour reminder for an 8am visit would fall due at 5am, so it waits until 7am.
  • Follow-up texts go out between 9am and 7pm. One that falls due overnight waits for 9am.
  • The morning-of reminder for a landlord's own repair goes out from 7am until the appointment.

Two edge cases needed explicit rules. If a 24-hour reminder gets held overnight, it could land right on top of the 3-hour one, so each reminder's window ends where the next one's begins, and a missed 24-hour reminder is simply dropped. And if a job is booked within an hour of when a reminder would go out, that reminder is skipped, because everyone just got the booking text. A job booked tonight for 10am tomorrow gets the 3-hour reminder but not the 24-hour one.

"7am" has to mean 7am where the property is. Our servers run on UTC, and an appointment agreed over text as "Thursday at 2" means 2pm local. So appointment times are stored as wall-clock times in the landlord's timezone, which landlords pick during onboarding (it defaults to their browser's timezone) and can change in settings. Every reminder check compares against that landlord's local clock, never the server's.

Running reminders on a server that sleeps

The obvious way to run reminders is a timer inside the app that checks every few minutes. That doesn't work for us. Doot's backend runs on Cloud Run, which scales to zero when nobody's using it. A 7am reminder would only fire if something happened to keep a server awake at 7am. And when traffic picks up, there can be up to three instances, so an in-app timer would run three times.

Instead, Google Cloud Scheduler calls a single "run the sweeps" endpoint every 15 minutes. Each sweep is independent, so one failing doesn't skip the others. The ones that matter here:

  1. The completion check after an appointment time passes.
  2. Appointment and morning-of reminders.
  3. Follow-up texts and the landlord's "still waiting" email.

The scheduler authenticates with a Google-signed token for a dedicated service account, which the backend verifies, so no shared secret has to live in the scheduler's configuration. In local development, a small in-process scheduler runs the same sweeps. The 15-minute cadence is why the product promises reminders "around" a time, not to the minute.

Never sending the same reminder twice

The worst bug a reminder system can have is texting a tenant the same thing twice. Overlapping runs are realistic here: a scheduler retry, a slow sweep that runs into the next one, or a manual run during debugging.

So no reminder is sent on the strength of a read. Each one is claimed first with a single conditional database update. For an appointment reminder, that means "mark the 24-hour reminder as sent, but only if it isn't marked already." The database applies that update for exactly one caller, and only the caller whose update succeeded sends the text. Everyone else sees zero rows changed and moves on.

Follow-ups use the same idea with one twist. The claim only succeeds if the "last contact" time is still the value the sweep just read. If the tenant texts in between the read and the claim, their reply moves that time, the claim fails, and no follow-up goes out right after they've answered.

Sending is per recipient. If one person's number fails, the failure is logged on the job's timeline and the other person still gets their reminder.

What the landlord sees

Landlords aren't texted about decisions they need to make. Those go out as an email with a link straight to the job. The exception is when the landlord is doing the repair themselves, in which case they're texted like any contractor would be.

Every reminder, follow-up and email is also written to the job's timeline, the same history that holds the tenant's texts and contractor calls. The job page updates live when that happens: the change is broadcast through Postgres to every running server, so a reply that lands on a different server than the one serving the landlord's open page still shows up without a refresh.

What's next

The reminder timings, 24 hours and 3 hours before a visit, are fixed for now, and only the follow-up pace is configurable. Landlords who want a different rhythm, or a text instead of an email when a job is stuck, are the obvious next requests.

If you're running repairs by hand, here's the full process for handling maintenance requests as a landlord, or see how Doot handles them for you.

FAQ

What should an appointment reminder text say?

Who is coming, when, and what for, plus anything the person needs to do, such as being home to let them in. End with an easy way to change the plan. Doot's version reads: "Doot: reminder -- Mike's Plumbing is coming tomorrow at 2:00 PM for "Kitchen sink leak". Please make sure someone's home to let them in. Let me know if you'd like me to reschedule."

How far in advance should you send an appointment reminder text?

Two reminders work well for repair visits: one about 24 hours before, so there's time to reschedule, and one about 3 hours before, as a same-day heads-up. If the appointment was booked less than an hour before a reminder would go out, skip that reminder, since the booking confirmation just covered it.

What time of day should reminder texts be sent?

Only during waking hours in the recipient's timezone. Doot sends appointment reminders between 7am and 9pm and follow-up texts between 9am and 7pm, both in the landlord's local time. A reminder that falls due overnight waits until the morning instead of going out at 2am.

How do you send automated text reminders without annoying people?

Send one reminder per question rather than one per hour, let the recipient control the pace, and stop the moment they reply. In Doot, any reply pushes the next follow-up a full interval out, even one that doesn't answer the question, and the landlord picks how often Doot follows up on each job: every 6, 12, 24 or 48 hours, or not at all.

How do you stop automated reminders from sending twice?

Claim the reminder in the database before sending it. Doot records a reminder as sent with a single conditional update that only succeeds if nobody else has already recorded it, and only the process whose update succeeded sends the text. Two overlapping runs can't both send.

Have a take? Reply at marcher@yaswedo.com.

If you'd rather Doot send the reminders and chase the replies, it's free to start.

Try Doot Free

Stack is FastAPI, Next.js, Postgres, Twilio, Retell, Cloud Run if anyone's curious.