Watch a 5-minute live demo — no sales call required
Back to Blog
EngineeringAISchedulingEngineering

How Driive Built the Scheduling Engine for AI Agents

July 5, 20268 min read
Illustration of a deterministic scheduling engine behind an AI booking agent — availability bitmaps, drive-time routing, and server-minted slot IDs
Driive
Corey Collins

Corey Collins

CTO & Co-Founder

Corey Collins is CTO and co-founder of Driive, where he leads the engineering behind the scheduling infrastructure that powers AI booking agents for home-service businesses.

Learn more about our team
Quick Answer

LLMs should never compute a time. In Driive's architecture the language model only talks (it relays preferences and picks from options) while a deterministic engine computes availability, ranks slots, and mints the only slot IDs the agent is allowed to book. Instead of validating away invalid bookings, the architecture makes them structurally impossible.

Share:

There's a moment in every "AI books your appointments" demo where the founder holds their breath. The voice agent says "Great, I've got you down for Tuesday at 2pm!" and everyone claps. Somewhere, a dispatcher quietly checks whether Tuesday at 2pm actually exists, whether the technician can physically drive there from their 12:30 job, and whether the AI just double-booked the only certified electrician in the county.

We built Driive so nobody has to hold their breath.

Here's the thesis, and it's going to sound almost insultingly simple: LLMs should never be allowed to compute a time. Not even "with guardrails." The model talks; a deterministic engine schedules. Everything interesting about our architecture falls out of taking that sentence seriously.

The problem with letting the model do math

Language models are excellent at conversation and hilariously bad at the things scheduling actually requires: interval arithmetic across timezones, travel-time constraints between jobs, crew requirements ("two roofers plus one licensed electrician"), buffer rules, booking windows, and race conditions when two customers want the same slot. None of that is a hallucination edge case; it's the entire job.

So we split the world in two:

The conversation layer — voice and chat agents that gather information, answer questions, and relay preferences. Probabilistic, charming, and not trusted.

The scheduling core — a deterministic pipeline that computes availability, ranks slots, and executes bookings. Boring, correct, and in charge.

The AI connects to the core over MCP (Streamable HTTP), and every tool it can call is a narrow, Zod-validated door into the engine. The agent doesn't get an API where it submits a time. It gets an API where it asks for options and picks one of the options it was given. More on that trick in a minute — it's the load-bearing wall.

Availability is just bitwise math (if you're brave enough)

Under the hood, a technician's day is a 96-bit bitmap: one bit per 15-minute slot, 0 for free, 1 for busy. Working hours are a bitmap. Calendar events are a bitmap. Buffer time widens an event's bits on both sides. And "when is this person actually available" becomes a single bitwise OR, where any layer that says busy wins:

sql
(working_hours_bitmap | busy_bitmap) AS available_bitmap

That's it. That's the algorithm. Postgres folds every calendar event into a per-day busy bitmap with bit_or, merges it with working hours, and a SQL function extracts the contiguous gaps as availability windows. The question "find every gap long enough for a 90-minute job, across 40 technicians, over the next 14 days" is one query, not a loop in application code that pages through calendars and prays.

Settings resolution rides in the same query. Working hours, service areas, and buffer rules can be defined at four levels — organization, team, team member, or a specific membership — and we resolve "most specific non-null value wins" per field, set-wise, in SQL:

sql
-- per field, take the most specific level that set a value
(array_agg(monday_bitmap ORDER BY specificity ASC)
  FILTER (WHERE monday_bitmap IS NOT NULL))[1] AS monday_bitmap

A 750-line CTE is intimidating, sure. It's also faster, more consistent, and vastly more testable than the same logic smeared across a fleet of microservices.

Drive time: compute per window, inherit per slot

Home services has a constraint that pure-calendar products get to ignore: your technician is a physical object that moves through traffic. A slot isn't bookable just because the calendar is empty: the tech has to be able to get there from the previous job, and still leave in time for the next one.

The naive approach computes drive time per candidate slot. A 4-hour open window at 15-minute granularity is 16 slots, which means 16 routing calls for information that's nearly identical. So our pipeline computes travel time once per availability window, and every slot expanded from that window inherits it. Then each slot passes a two-sided reachability check:

ts
// earliest arrival: previous job's end + drive + buffer
const earliestReachableMs =
  previousAppointmentEnd + driveFromPreviousMs + previousBufferMs;

// and the slot must still leave room to reach the NEXT job
if (slotEnd + driveToNextMs + nextBufferMs > nextAppointmentStart) {
  // physically impossible — discard
}

Drive-time estimation itself is a pluggable strategy. A Haversine estimate (straight-line distance × a 1.4 circuity factor at ~40 km/h) is free and offline, perfect for cheap pre-filtering. Traffic-aware Google Routes matrices come in for the final ranked search, batched and deduped so we make one external call per direction instead of hundreds. Same interface, different cost profile, swap at will.

The gap penalty: encoding dispatcher intuition as a piecewise function

Gap between jobsPenalty
0 min0.00 — perfect, back-to-back
30 min0.15 — fine, that's a buffer
60 min0.95 — the dead zone
90 min0.25 — recovering
120+ min0.70 — neutral

Once we have feasible slots, we rank them. Most factors are what you'd expect: drive time weighted 45%, temporal proximity 45%, alignment bonuses. The fun one is the gap penalty, and it's deliberately non-monotonic:

Ask any dispatcher: a one-hour hole between jobs is the worst outcome. Too long to wait in the truck, too short to sell to another customer. A 30-minute gap is a buffer; a 90-minute gap is inventory. So the penalty curve goes up and comes back down, exactly like the intuition does. This is the kind of domain knowledge an LLM will confidently get wrong every time, and a five-point piecewise-linear function gets right for free.

Multi-person jobs go through a constraint solver too: "at least two from the roofing team plus one certified electrician" is resolved by finding the minimum satisfying crew per candidate time (inclusive counting: a required specialist also counts toward their team's minimum, because paying three people to do a two-person job is not a growth strategy). If a time can't be staffed, the slot doesn't exist. The AI never sees it.

The trick that makes it safe: server-minted slot IDs

Here's the part I'd tattoo on every "agentic booking" pitch deck.

When the AI calls our slot-search tool, driive_search_slots, the pipeline runs, ranks the results, and caches each slot server-side under an opaque slotId. The agent gets back human-readable options and those IDs. When it calls driive_book_appointment, the tool will only book a slot it can re-read from that cache:

ts
const cachedSlot = await slotCache.get(`${ctx.orgId}:${input.slot_id}`);
if (!cachedSlot) return failure("slot_expired_or_invalid", ...);

The times, the team member assignments, the timezone: all of it comes from the cached, server-computed slot, not from the model. The agent is structurally incapable of inventing a time. It can't book 2am, or a slot for a technician on vacation, or anything else the engine didn't explicitly mint, because the only currency it holds is IDs the engine issued.

This pattern generalizes, and I think it's the actual answer to "how do you make agents safe": don't validate the model's output; make invalid output unrepresentable. Capability tokens beat guardrail prompts.

The rest of the boundary follows the same philosophy:

Org isolation by closure, not by trust. Every MCP request gets a fresh, org-scoped server instance; tools close over the org context. There is no argument an LLM could pass to reach another tenant's data, because tenancy isn't an argument.

Zod at every door. A customer's flexibility window (how far they're willing to shift around a preferred time) has to be an integer between 30 and 720 minutes. Datetimes must carry offsets. Malformed input dies at the boundary with a structured error, not deep in the engine.

Status gates. You can't book a request that isn't ready to be scheduled. You can't book an in-person job without a geocoded address. The conversation flow is a state machine expressed as tool prerequisites; the model can't skip steps because the steps won't answer out of order.

Races degrade gracefully. Two callers grab the same slot? The loser's tool returns a soft conflict with scripted recovery language ("that time was just taken — let me find you another") and a nudge to re-search. No stack traces read aloud to a customer on the phone.

The tools steer the model back. Every tool response includes next_step hints and presentation guidance (including "never expose slot IDs or internal ranking scores to the customer"). Rails in the outputs, not just a 4,000-token system prompt the model half-remembers.

The division of labor, stated plainly

The LLM decides what the customer wants. The engine decides what is true.

Job duration comes from qualification rules the business configures, not vibes. Crew selection comes from a constraint solver, not the model's sense of who seems available. Ranking comes from a scoring function a human tuned against real dispatcher behavior. The model's entire scheduling authority is: relay a preference, present the options it was handed, and pick an ID.

And when a booking lands, the engine invalidates its availability caches and emits durable events, so the next search — from an AI agent, a booking widget, or a human dispatcher — sees fresh data. External calendars sync back in through the same pipeline. One source of truth, many interfaces, zero of which are allowed to do arithmetic on their own.

AI agents are a spectacular new interface to scheduling and a catastrophic implementation of it. Build accordingly.

Building agents that book real appointments?

Driive is scheduling infrastructure for home-service businesses — and for the AI agents that increasingly answer their phones. If you're building agents that need to book real appointments with real humans and real trucks, we'd love to talk.

Cite This Article

Corey Collins. (2026, July 5). How Driive Built the Scheduling Engine for AI Agents. Driive. https://getdriive.com/blog/scheduling-engine-for-ai-agents