Building an appointment-booking agent, prompt-first
A booking agent is mostly rules about when not to book. We take one from a vague one-line brief to a spec the checker passes, letting the compiler catch the two mistakes almost everyone makes on the way.
“Build an agent that books dental appointments.” That’s the brief, and it’s the kind of brief you actually get. Prompt-first means we don’t start in application code; we start by writing the agent’s contract in a .tg file and running the checker after every change, the way you’d run a type-checker. The point isn’t the finished prompt. It’s watching where the compiler stops you, because those are the exact spots where a hand-written prompt would have shipped a bug.
§1Turn the brief into rules
“Books appointments” is a goal, not a spec. The spec is the constraints around it: what the agent must confirm first, what it must never do, and what happens when it can’t. Talk through the brief and the real rules fall out fast. Confirm who you’re booking for. Don’t book blind. Offer a bounded number of options so a voice caller isn’t read a timetable. Have an answer for a full calendar. Write those down before writing anything clever.
Here’s the honest first pass, typed straight from the conversation. It’s the version everyone writes, and the checker has an opinion about it already.
# Role You are a scheduling agent for a dental clinic. # Constraints - SHOULD try to be helpful and book things quickly. - MUST keep replies friendly and professional. - Book appointments when the patient wants one.
booking.tg:5:10 info prompt/hedging Hedging - "try to" turns this instruction into a suggestion the model may skip. Delete the hedge, or commit to a modal (MUST / NEVER / SHOULD). ✓ 1 file - 0 error, 0 warning, 1 info
So the first pass isn’t a spec, it’s a mood board. Rewrite each line as one instruction with a concrete, checkable action or bound: confirm the full name, cap the options at three, never book without a confirmed slot, offer the waitlist when nothing’s open. Vague adjectives out, testable verbs in.
§2Wire the tool without leaking its name
Booking is a side effect, so the agent needs a tool. Define it with $TOOL (name, description, typed inputs, the HTTP binding) and then tell the model when to call it. This is where the second near-universal mistake lives: you reference the tool by typing its name into a rule, like a variable you forgot to declare.
# Instructions - WHEN the patient confirms a slot THEN call book_appointment. $TOOL book_appointment - description:: Books a confirmed slot for a patient. - input:: - patient_name:: string - slot_id:: string - request:: POST https://api.clinic.example.com/appointments
booking.tg:2:46 error structure/bare-tool-ref "book_appointment" is a tool, but this mention is bare prose - the compiler cannot bind it. Point it with @[book_appointment] (a checked reference), or quote it to speak the name as plain text. ✗ 1 file - 1 error, 0 warning, 0 info
@[book_appointment] and the mention is now checked: rename or delete the tool and this rule fails to compile until you fix it too.This looks pedantic until the tool gets renamed six months from now. A bare-prose mention drifts silently; a checked @[…] pointer turns the rename into a compile error that points you straight at every rule that used it. The compiler is asking you to declare your dependencies, for the same reason your linter does.
§3Pin the behavior with a test
The rules compile now, but “compiles” only means the spec is internally consistent, not that the agent does the right thing on a hard turn. The hard turn for a booking agent is the impatient user: “I want an appointment tomorrow,” with no name and no slot. The rule says confirm the name first; a $TEST block written next to that rule makes the requirement executable, so a future edit that loosens it breaks the build.
# Role @@ role: a scheduling agent, booking-only, no clinical advice You are a scheduling agent for a dental clinic. # Constraints @@ verify_name: the name keys the patient record, so confirm it before any write - MUST confirm the patient's full name before booking. @@ slot_cap: three options fit in one voice turn without overwhelming - MUST offer at most 3 open slots at a time. @@ no_blind_book: a booking without a chosen slot double-books the calendar - NEVER book an appointment until the patient has confirmed a specific slot. @@ waitlist_fallback: an empty calendar still needs a next step - IF no slots are open THEN offer the waitlist. @@ book_it: once a slot is confirmed, write it through the tool - WHEN a slot is confirmed THEN call @[book_appointment]. $TOOL book_appointment - description:: Books a confirmed slot for a patient. - input:: - patient_name:: string - slot_id:: string - request:: POST https://api.clinic.example.com/appointments $TEST asks_name_first - input:: I want an appointment tomorrow. - expect:: - asks for the patient's full name - does not call the tool yet
@@ note saying why it exists, the tool is a checked reference, and the sad path is pinned by a test. This is the whole agent contract, and it compiles.booking.tg - B (89/100) proven errors: none tiers: base+z3
planes runtime 85 (what the model reads) · hygiene 100 (source only)
lever enforceability 60/100 (up to +11 overall) - Write rules as
MUST / NEVER with concrete bounds, not vague qualities.
§4What you actually shipped
Four passes and the brief became a contract: a role, five rules that each say exactly one checkable thing, a tool the compiler tracks by reference, and a test that fails the build if the agent stops asking for a name. Nothing here is booking-specific magic. It’s the same loop for a returns agent, a triage router, or an order-status bot: write the rule, run check, fix what it proves, and leave a $TEST behind so the rule can’t rot.
Prompt-first isn’t slower. The two errors we hit, a hedged modal and a bare tool reference, are bugs you’d otherwise find in production, in a transcript, weeks later. We found them in seconds, before a single request was served. That’s the trade the whole practice is built on.
Everything here runs on the tools that shipped in TypeGlish 0.1.0: $TOOL definitions, checked @[tool] pointers, and inline $TEST blocks. Run tg check and tg score on your own agent prompt to see where it stands.