Khela Tech builds AI and workflow automation for Canadian small businesses. This is a write-up of one system we run in production: the operations platform behind a Toronto-area pet-boarding business. It is not a product tour. It is the one design decision that kept the system reliable as it grew past 150 scheduled jobs, and the rules that came with it.
The problem
A boarding business runs on small, repetitive tasks with hard deadlines: booking confirmations by SMS, reminders 48 hours before arrival, invoices, review requests, inventory reorders, client follow-ups, and the email that has to be read and filed into the CRM. None of it is hard on its own. All of it is easy to forget on a busy day, and every miss is a client who did not get told something.
The shape of the solution
One Python Flask control plane, backed by PostgreSQL, owns the business logic. Each automation is a scheduled job: one job, one purpose, one log line per run. Today that is 150+ jobs.
The interesting part is not the jobs. It is deciding where each job is scheduled.
Two schedulers, one question
Every recurring job lives in one of two places, and one question decides which:
Does this job need to run when the app itself is not running?
- Yes: the operating system's task scheduler. This is the small "infrastructure ring": the watchdog that relaunches the app, the nightly backups, network failover, anything that manages the app or must survive it being down.
- No: the in-app scheduler. This is the default for everything else: reports, reminders, syncs, monitors. These jobs only make sense when the app is up, and they share its database connection, locks and in-memory state.
It sounds obvious. The failure modes when you ignore it are not:
- A backup job living inside the app does nothing on exactly the night the app has crashed, which is the night you needed it.
- A reminder job moved out of the app to "make it independent" loses the shared state and locks it was quietly relying on, and starts double-sending.
So the rule is enforced in one direction only: keep the infrastructure ring small and stable, and put every new business job in the app. We do not blanket-migrate one ring into the other.
One pane of glass
Two schedulers means two places to look, which is how jobs get forgotten. A read-only dashboard lists both side by side, live: the in-app job list and the OS scheduled tasks, split into the two rings, with last and next run times. When something is late, there is one page to open.
Make every job boring to operate
A few habits matter more than any framework choice:
- Every job logs, retries and reports. A job that fails quietly is worse than one that never existed, because people stop checking.
- Alert on staleness, not just on exceptions. Some of the worst failures never raise: the job ran, did nothing, and exited 0. A monitor that asks "when did this last do real work?" catches those.
- Background means background. Scheduled tasks run hidden. A console window flashing up on the operator's screen every few minutes is how someone ends up closing the wrong window.
- A watchdog owns restarts. Nobody restarts the app by hand. The watchdog relaunches it, so there is never a second copy running the same 150 jobs twice.
- Plain-English alerts. A routine success is one sentence or nothing. Detail is for when a human has to act.
The outcome
One founder runs the business without an operations team. The system does the admin; people do the work that needs a person. When a job breaks, the monitoring says so before a client notices.
Stack
Python, Flask, PostgreSQL, the in-app schedule library, Windows Task Scheduler, Resend for email, OpenPhone for SMS, QuickBooks, Stripe, Google Workspace and Sentry.
Khela Tech is a Toronto AI automation studio for Canadian small businesses: voice agents, AI agents, workflow automation and custom integrations, each shipped with monitoring and a runbook. More live systems: khelatech.ca/case-studies.
Top comments (0)