For the past 6 months, we've been running an AI-powered booking system. Customers would text us, an AI agent named Steve would qualify them, another AI agent named Anna would manage the calendar and find slots, and a third agent named Dev would handle follow-ups and reminders.
It worked. Kind of. The pipeline had 6 compliance gates, a double-gated token locker, GPG encryption, and bootstrap hash verification. Over 3,900 lines of bash across 9 scripts. Three Docker containers. It was technically impressive — a proprietary agentic system that could draft SMS replies, detect booking intent, find available slots, and send confirmations.
But here's what we learned: for post-booking automation, AI is complete overkill.
The Problem
Three AI agents. Three Docker containers. Three queue tables. Three sets of bootstrap files. Every booking confirmation, reminder, and follow-up had to pass through the full 6-gate compliance pipeline — token locker and all — just to send "Your booking is confirmed at 10:30am."
The complexity created real, production-breaking problems:
- Token locker failures — bootstrap hashes went stale every time any file was edited, blocking all SMS sends until someone manually reset them
- Database divergence — multiple copies of crm.db drifted apart, so bookings created in one place were invisible to Steve in another
- Agent misclassification — the AI would sometimes classify "How much for a split system?" as an emoji reaction and refuse to reply
- Docker inode drift — bind-mounted files would go stale after edits, causing hash mismatches between host and container
- Anna was redundant — the calendar agent's entire job was "hold a slot for 4 hours." The customer never interacted with her. She just sat there, polling a queue, running a 6-gate pipeline to delegate a slot_hold action to herself.
- Dev barely existed — the follow-up agent was planned but never fully operational. The architecture was so heavy that building simple reminder sequences felt impossible.
Every time we wanted to change a reminder message, it meant editing bootstrap files, recomputing MD5 and SHA256 hashes, restarting Docker, clearing caches, and hoping the token locker would re-register. Changing one word in one SMS required touching 4 files.
The New System: One Table, One Daemon
We archived Anna entirely. We removed the hold system, the slot tracker, the anna_queue, the calendar bridge in Gate 5. Then we built something so simple it's almost embarrassing to explain:
The message_queue Table
CREATE TABLE message_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, phone TEXT NOT NULL, message TEXT NOT NULL, template_key TEXT, scheduled_at DATETIME NOT NULL, status TEXT DEFAULT 'pending', sent_at DATETIME, booking_id INTEGER );
That's it. A single SQLite table. Rows get inserted with a phone number, a pre-written message, and a future delivery date. A PM2 daemon polls every 30 seconds, finds pending messages where scheduled_at <= now, sends them directly via the Join SMS API, and marks them done.
No AI. No gate pipeline. No token locker. No Docker containers. No bootstrap hashes. Just rows in a table and a 30-second loop.
What Happens When Someone Books
When a customer books at myairconcare.com/book, six messages get queued automatically:
- Confirmation — immediate: "You're booked in! 🎉" with details, what to expect link, and a change/cancel link
- 2-month reminder — loyalty discount reminder with keep-or-cancel link
- 1-month reminder — final discount lock-in, keep-or-cancel link
- 48-hour reminder — prep instructions, job info link
- Morning-of — 7am on the day: "We're on our way!"
- Thank you — 4pm after: review link + one-click rebook for next year
Every message includes a link back to the booking page where customers can self-serve — change dates, cancel (with a discount warning popup), or rebook for next year. Steve handles inbound SMS only. Everything else is links and automation.
The Daemon Itself
message-daemon.js — Core Logic
const CURFEW_START = 480; // 8am AEST
const CURFEW_END = 1439; // 11:59pm AEST
const MAX_RETRIES = 3;
const DEDUP_WINDOW_MIN = 5; // minutes
async function processQueue() {
if (!inSendingWindow()) return;
const messages = await dbAll(
`SELECT * FROM message_queue
WHERE status='pending'
AND scheduled_at <= datetime('now','+10 hours')
ORDER BY scheduled_at LIMIT 5`);
for (const msg of messages) {
// Anti-spam: max retries, dedup check
// Send via Join API directly (no gate, no token)
// Mark sent or retry
}
// Daily brief at 9pm — tomorrow's jobs with Apple Maps links
}
The daemon runs as a PM2 process. It enforces curfew (8am-11:59pm AEST), prevents spam (max 3 retries, 5-minute dedup window), and every night at 9pm generates a daily brief with tomorrow's bookings and Apple Maps links sent directly to Jason.
Compare this to the old system: Gate 0 (fingerprint 6 bootstrap files) → Gate 1 (context check) → Gate 2 (draft validation) → Gate 3 (8-point compliance) → Gate 4 (double-token GPG handshake with PC locker) → Gate 5 (CRM log + Anna delegation) → Gate 6 (clearance token). All of that. To send one reminder SMS.
What We Gained
- Zero AI failures — messages are pre-written templates with customer details merged in. No misclassification, no hallucinated dates, no "emoji reaction" false positives.
- Instant editing — change a reminder message by editing one string in one file. No hashes to recompute. No Docker to restart.
- Built-in safety — curfew enforcement, dedup protection, retry limits. The daemon physically cannot spam anyone.
- One canonical database — everything points to crm.db. 3,988 contacts. No divergence, no split-brain.
- Self-service booking — customers book, change, cancel, and rebook through links. Steve handles inbound SMS conversations only.
- PM2 managed — no cron jobs. The daemon runs as a PM2 process with automatic restart on failure.
- Apple Maps links — every daily brief includes one-tap navigation to every job address.
Steve Is Still Here
We didn't kill Steve. He's still the front-line SMS agent — qualifying leads, quoting prices with exact dollar amounts, and when someone's ready to book, sending them a personalized link to the booking page. He doesn't manage the calendar anymore. He doesn't "hold slots" or "check availability" or "delegate to Anna." He just talks to customers and sends links. His bootstrap files dropped from 6 to 4 active files. His pipeline still runs through all 6 gates — but Gate 5 no longer delegates to a non-existent calendar agent.
The booking page handles slot selection, payment (3 tiers: pay in full with $30 off, $50 deposit, or pay on day), and confirmation. The message daemon handles reminders and follow-ups. Steve handles conversation. Each piece does one thing well.
The Same Pattern Replaces Follow-Ups Next
The most exciting part: this exact same system replaces our entire follow-up pipeline. Instead of scheduling messages months and days out, we use seconds and minutes. Cold lead autoresponders. Nurture sequences. Re-engagement campaigns. All driven by the same message_queue table and the same 60-line daemon.
No Dev agent. No followup_queue. No separate Docker container. Just rows in a table with scheduled_at timestamps.
The Lesson
AI is incredible for conversations. It's terrible for scheduling. When you need reliability, simplicity, and zero hallucination risk, a SQLite table and a 30-second loop beats a 6-gate AI pipeline every single time.
We built an agentic calendar manager because it sounded cool. We replaced it with a database table because it actually works.
The full system — self-service booking page, Steve SMS agent, message daemon, 6-message lifecycle, one-click rebook, discount-protected cancel flow, daily brief with Maps links — was built in a single 14-hour session. No new infrastructure. No external services. Just Node, SQLite, PM2, and Join.
Book Your Air Con Clean Online