← Tidelines/Guides

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.

by TypeGlish team8 min read#guides
From a one-line brief to a spec that checks.

“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.

booking.tg (first pass)⚠ advisory
# 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.
Three rules, and only one of them is really a rule. The checker flags the hedge; the other two are quietly worse.
tg check - output
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
“SHOULD try to” hedges a modal that’s already soft. But look past the one flag: “be helpful,” “friendly and professional,” and “when the patient wants one” aren’t checkable at all. They read fine and constrain nothing.

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.

booking.tg (wiring the tool)✗ blocked
# 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
The rule names the tool in bare prose, so the compiler can’t bind the mention to the definition. It stops the build instead of guessing.
tg check - output
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
Point the reference with @[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.

booking.tg✓ compiles · B (89/100)
# 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
Every rule carries an @@ 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.
tg score - output
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.
A B, and the score names its own next lever: enforceability. The two remaining soft spots are “helpful”-flavored verbs the checker still can’t see into. That’s the honest ceiling of this draft, not a failure.

§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.

Field note

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.

∿ washed up Jul 16, 2026 ∿