← Tidelines/Guides

Building a flight-disruption agent, prompt-first

A cancelled flight puts a few hundred people into the chat widget at once, and every one of them wants the same three things: a seat, a meal, and a number. The brief from disruption ops covers all three in six lines. Two of those lines disagree about who gets to move a ticket.

by TypeGlish team8 min read#guides
Rebooked on silence.

TL;DR A disruption agent built from the brief as written would rebook passengers without asking, because rebook passengers on the next available flight becomes an unguarded MUST call @[rebook_flight] that the compiler proves contradicts the consent rule at the bottom of the same brief. Write consent as a WHEN / UNLESS pair on the tool, let the booking system hand over the delay as a number, and the exhaustiveness check names the passenger delayed exactly 180 minutes, the one nobody had decided about. The brief is D (59/100); the spec is A (100/100).

Larkspur Air is a fictional short-haul carrier with a real problem: on a bad weather day the disruption desk takes more chats in an hour than it does in a normal week. The plan is an agent, Robin, that handles the first contact for every delayed or cancelled passenger: finds them a new flight, tells them what they are owed, and hands compensation to the claims team. The head of disruption ops sends the brief below. Here is what the compiler makes of it, and the three decisions that turn it into a spec.

§1The brief, checked as written

Every line of this is how a disruption desk actually talks. None of it is wrong as policy.

brief.tg - the six lines as sent✗ D (59/100)
# Role
You are a helpful assistant for Larkspur Air that helps passengers whose flights are disrupted.

# Constraints
- Rebook passengers on the next available flight.
- Don't promise compensation.
- If the delay is over 3 hours offer a meal voucher.
- Passengers can choose a refund instead if their flight is cancelled.
- Try to keep them on Larkspur flights where possible.
- Never confirm a new flight until the passenger agrees to it.
Six constraints: a rebooking policy, a compensation rail, a meal entitlement, a refund option, a commercial preference, and a consent rule.
tg score brief.tg - output
$ npx typeglish score brief.tg
brief.tg — D (59/100)  proven errors: none  tiers: base+z3
  planes  runtime 64 (what the model reads) · hygiene 45 (source only)
  facets  enforceability 28 x.21 · hardness 68 x.12 · directness 69 x.08 · consistency 100 x.17 · structure 89 x.12 (hygiene) · annotation 0 x.12 (hygiene) · style 36 x.08 · security 100 x.08
  lever   enforceability 28/100 (up to +15 overall) — 5 rule-shaped lines the checker reads as prose (L5 L6 L7 L8 L9): lead with a modal (MUST / NEVER / SHOULD); 1 hard rule it cannot prove (L10): one action, one object, a catalog verb or a bound. The rules table below has the edit per line
  rules   1 of 6 rule-shaped lines read · 1 not provable
    L5   ✗ invisible     Rebook passengers on the next available flight.
                         no modal (MUST / NEVER / SHOULD) and "Rebook" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L6   ✗ invisible     Don't promise compensation.
                         no modal (MUST / NEVER / SHOULD) and "Don't" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L7   ✗ invisible     If the delay is over 3 hours offer a meal voucher.
                         the condition has no THEN branch, so nothing is required when it holds → add THEN MUST / NEVER / SHOULD <verb> …
    L8   ✗ invisible     Passengers can choose a refund instead if their flight is cancelled.
                         no modal (MUST / NEVER / SHOULD) and "Passengers" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L9   ✗ invisible     Try to keep them on Larkspur flights where possible.
                         no modal (MUST / NEVER / SHOULD) and "Try" is not a verb the checker knows, so it reads as prose → lead with a modal, or start with a verb from the catalog (Escalate …, Ask …)
    L10  ~ unprovable    Never confirm a new flight until the passenger agrees to it.
                         the checker knows "confirm" but cannot prove this shape: it stops reading at "until" — the clause after it is not part of the provable action → if the clause is a condition, make it one: WHEN <clause> THEN MUST <verb> …; otherwise give it its own rule
  L2  prompt/agentless-passive     −0.25  The doer is deleted (agentless passive) - who performs this? Name the actor or
  L7  typeglish/if-then            −0.25  IF needs a THEN — write IF <condition> THEN <action>.
  L7  prompt/unintroduced-definite −0.25  "the delay" retrieves something this document never introduces - a model must
  L8  prompt/unregistered-doer     −0.25  A bare generic doer never enters the world model - instruction to the agent, o
  L9  prompt/hedging               −0.25  Hedging — "Try to" turns this instruction into a suggestion the model may skip
  L9  prompt/hedging               −0.25  Hedging — "where possible" turns this instruction into a suggestion the model 
  L9  prompt/unregistered-doer     −0.25  A bare generic doer never enters the world model - instruction to the agent, o
One of six lines read, and that one, the consent rule, is ~ unprovable because it stops reading at until. Note L6: Don't is not DO NOT. The contraction reads as prose.

The grade is not the interesting part. The interesting part is that consistency 100: with five of six lines invisible, there is nothing for anything to disagree with. That is the same false calm the starter prompt that came with the platform ends on, and it lasts exactly until the lines become rules.

§2Rebook is a tool call, and it needs consent

The first edit is the mechanical one: give every line a modal and a verb the checker knows. Rebook is not in the catalog, and it should not be, because rebooking is not something Robin says. It is something the booking system does, so it becomes a tool, and the rule becomes a call.

rules.tg - the brief, promoted line for line✗ logic/contradiction
$TOOL rebook_flight
  - description:: Move the passenger onto a new flight.
  - input::
    - booking_ref:: string: the six-character booking reference.
    - flight_number:: string: the flight the passenger accepted.

# Role
You are Robin, the disruption assistant for Larkspur Air.

# Context
A passenger contacts you when Larkspur Air delays or cancels their flight.

# Constraints
- MUST call @[rebook_flight].
- NEVER state a compensation amount.
- WHEN the delay is more than 3 hours THEN MUST offer a meal voucher.
- WHEN Larkspur Air cancels the flight THEN MUST offer a refund.
- UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight].
Line 5 of the brief became line 14; line 10 of the brief became line 18. Don't promise compensation became never state a compensation amount, because promise is not a verb anything can observe and an amount is.
tg check and build - rules.tg✗ nothing written
$ npx typeglish check rules.tg
rules.tg:14:1  error  logic/contradiction  Conflicts with line 18. Logical conflict — "call
  @[rebook_flight]." is both required and forbidden. Keep one, or scope the two rules so they
  cannot both apply (IF <condition> THEN ...).
          ↳ line 18: - UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight].
rules.tg:18:1  error  logic/contradiction  Conflicts with line 14. Logical conflict — "call
  @[rebook_flight]." is both required and forbidden. Keep one, or scope the two rules so they
  cannot both apply (IF <condition> THEN ...).
          ↳ line 14: - MUST call @[rebook_flight].

✗ 1 file — 2 error, 0 warning, 0 info

$ npx typeglish build rules.tg
typeglish build: rules.tg refused — nothing written
The rule that says rebook everyone, and the rule that says rebook nobody who has not agreed, are the same tool with opposite force. On any turn where the passenger has not said yes, both fire.

This is the brief's real bug, and it was there in the prose version too. Read the first line the way a model does: rebook passengers on the next available flight is an instruction with no condition, so it applies on the first turn, before anyone has been offered anything. The consent line at the bottom is what the ops lead actually meant, and the model was left to guess which one wins. In a chat widget, the guess that loses is a passenger who asked what are my options and got a boarding pass for a 06:10 departure.

The fix is to give the obligation the same condition the prohibition already has, so the two rules cover the two halves of one question and never the same turn:

the consent pair✓ clean
- WHEN the passenger accepts a new flight THEN MUST call @[rebook_flight].
- UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight].
Same guard, opposite polarity. The checker compares them and finds no turn where both apply. The full file is in §4.

Both halves matter. Drop the UNLESS line and the file is still clean, but silence is no longer covered: nothing forbids the call on a turn where the passenger has not answered. A write action is the one place a prompt should say both what triggers it and what does not, which is the checklist in your agent can now do something it cannot undo.

§3The agent does not know how late the flight is

If the delay is over 3 hours reads like a rule for Robin, but Robin has no departure board. The delay lives in the booking system, which knows the scheduled time, the new estimated time, and whether the flight has been cancelled outright. So the second decision is to stop asking the model to work it out, and have the host pass it in: one typed input for what happened, one number for how late.

chain.tg - the entitlement as a deterministic chain, first draft✗ structure/non-exhaustive-switch
$REQUIRE variable disruption: one of delayed, cancelled
$REQUIRE variable delay_minutes: number

# Role
You are Robin, the disruption assistant for Larkspur Air.

# Constraints
$IF @{disruption} is equal to cancelled:
  - MUST offer a refund.
$ELSE IF @{delay_minutes} is greater than 180:
  - MUST offer a meal voucher.
Over 3 hours became is greater than 180, the literal reading of over.
tg check chain.tg - output✗ 1 error
$ npx typeglish check chain.tg
chain.tg:8:1  error  structure/non-exhaustive-switch  This chain isn't exhaustive — no arm
  handles @{disruption}=delayed, @{delay_minutes}=180.0. Add an arm or an ELSE.

✗ 1 file — 1 error, 0 warning, 0 info
The compiler did not just say the chain has a gap. It produced a passenger who falls into it: delayed, by exactly 180 minutes.

That witness is worth more than the error. A passenger delayed exactly three hours is precisely the case the brief's over 3 hours leaves undecided, and it is also the case that gets escalated to a supervisor on the day, because the customer reads 3 hours or more and the desk reads more than 3 hours. The compiler cannot tell you which reading is policy. It can make sure somebody chooses one before the prompt ships.

Larkspur's ops lead chooses the strict reading and adds the arm that makes it explicit: a delay of three hours or less gets no voucher, and says so. An $ELSE arm that writes the negative down is also what stops Robin handing out vouchers on a 40-minute delay because a passenger asked nicely.

the chain, closed✓ exhaustive
$IF @{disruption} is equal to cancelled:
  - MUST offer a refund.
$ELSE IF @{delay_minutes} is greater than 180:
  - MUST offer a meal voucher.
$ELSE:
  - NEVER offer a meal voucher.

A number in a $IF chain has a known weakness, set out in building a returns-eligibility agent: the compiler cannot tell you that two thresholds are in the wrong order. With one threshold there is no order to get wrong, and the exhaustiveness check does the job a reviewer would otherwise have to do by hand.

§4The spec, and what one passenger’s model reads

The third decision is the smallest: the commercial preference, try to keep them on Larkspur flights where possible, leaves the prompt. It is a sort order, and sort orders belong in find_flights, which can return Larkspur departures first without the model weighing anything. What remains is nine rules, two tools, and three tests.

spec.tg - the disruption desk, finished✓ A (100/100)
$REQUIRE variable disruption: one of delayed, cancelled
$REQUIRE variable delay_minutes: number

$TOOL find_flights
  - description:: Find the next available flights for a disrupted booking.
  - input::
    - booking_ref:: string: the six-character booking reference.

$TOOL rebook_flight
  - description:: Move the passenger onto a new flight.
  - input::
    - booking_ref:: string: the six-character booking reference.
    - flight_number:: string: the flight the passenger accepted.

# Role
@@ role: one sentence names the speaker, so every later "you" is the agent
You are Robin, the disruption assistant for Larkspur Air.

# Context
@@ passenger: introduces the one party every rule below is about
A passenger contacts you when Larkspur Air delays or cancels their flight.
@@ claims_form: compensation is decided by the claims team, from a form
The claims form is at larkspur-air.example/claims.

# Constraints
@@ brevity: a stranded passenger reads on a phone in a queue
- MUST keep every reply to at most 3 sentences.
@@ options: the flight list comes from the system, never from memory
- WHEN the passenger asks for a new flight THEN MUST call @[find_flights].
@@ consent: a rebooking is a change to a ticket, so the passenger decides
- WHEN the passenger accepts a new flight THEN MUST call @[rebook_flight].
@@ no_consent: the other half of the consent rule, so nothing rebooks on silence
- UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight].
@@ no_amounts: an amount typed in chat reads as a promise
- NEVER state a compensation amount.
@@ claims: every compensation question ends at the form
- WHEN the passenger asks about compensation THEN MUST share the claims form.
@@ entitlement: the booking system computes the delay; the chain turns it into one line
$IF @{disruption} is equal to cancelled:
  - MUST offer a refund.
$ELSE IF @{delay_minutes} is greater than 180:
  - MUST offer a meal voucher.
$ELSE:
  - NEVER offer a meal voucher.

$TEST cancelled_refund
  - input:: My flight was cancelled, can I just get my money back?
  - expect::
    - offers a refund
    - at most 3 sentences

$TEST compensation
  - input:: How much compensation do I get for this?
  - expect::
    - shares the claims form
    - never states a compensation amount
    - contains "larkspur-air.example/claims"
    - matches /^(?!.*[0-9]+ (pounds|euros|dollars))/

$TEST no_silent_rebook
  - input:: What else is there tonight?
  - expect::
    - calls find_flights
    - does not call rebook_flight
The no_silent_rebook case is the whole of §2 as a test: an open question, where the right answer is a list of flights and the wrong answer is a changed ticket.
tg check, score and test --dry - spec.tg✓ 9/9 rules exercised
$ npx typeglish check spec.tg
✓ 1 file — 0 error, 0 warning, 0 info

$ npx typeglish score spec.tg
spec.tg — A (100/100)  proven errors: none  tiers: base+z3
  planes  runtime 99 (what the model reads) · hygiene 100 (source only)
  facets  enforceability 100 x.21 · hardness 100 x.12 · directness 95 x.08 · consistency 100 x.17 · structure 100 x.12 (hygiene) · annotation 100 x.12 (hygiene) · style 100 x.08 · security 100 x.08
  rules   9 of 9 rule-shaped lines read · 9/9 covered by $TEST

$ npx typeglish test spec.tg --dry
✓ spec.tg  coverage: 9/9 rules exercised
  · cancelled_refund — "My flight was cancelled, can I just get my money back?" (not run)
      ✓ at most 3 sentences
      · rubric — 1 prose expectation, judged on a live run (needs ANTHROPIC_API_KEY)
  · compensation — "How much compensation do I get for this?" (not run)
      ✓ contains "larkspur-air.example/claims"
      ✓ matches /^(?!.*[0-9]+ (pounds|euros|dollars))/
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
  · no_silent_rebook — "What else is there tonight?" (not run)
      · rubric — 2 prose expectations, judged on a live run (needs ANTHROPIC_API_KEY)
✓ 1 prompt — 0 failed
Coverage counts shared wording, so offers a refund also touches the voucher arms through offer. The deterministic asserts run offline; the prose expectations wait for a live run with a key.

Now bind the inputs the way the booking system will, once per passenger, and look at the last line of the artifact either side of the threshold:

tg build spec.tg - three passengers✓ three artifacts
$ npx typeglish build spec.tg --vars '{"disruption":"delayed","delay_minutes":180}'
✓ built d/spec.txt ← spec.tg (fb13677e11d1, full)
$ tail -1 d/spec.txt
- NEVER offer a meal voucher.

$ npx typeglish build spec.tg --vars '{"disruption":"delayed","delay_minutes":181}'
✓ built d/spec.txt ← spec.tg (9e4328e72954, full)
$ tail -1 d/spec.txt
- MUST offer a meal voucher.

$ npx typeglish build spec.tg --vars '{"disruption":"cancelled","delay_minutes":0}'
✓ built d/spec.txt ← spec.tg (fcdcd5960c6c, full)
$ tail -1 d/spec.txt
- MUST offer a refund.
One minute apart, two different prompts, two different hashes. The model serving the 180-minute passenger reads exactly one line about vouchers, and it is a prohibition, so there is no permission in the prompt to be talked into.

That is the payoff of moving the arithmetic out: the entitlement is decided by the compiler from the booking record, before the conversation starts, and the passenger's model reads one line about it. It is the same move as the number in the prompt vs. the number at runtime, with the added benefit that the boundary case was found by a proof rather than by the first angry passenger.

§5Common questions

How do I write a system prompt for a flight rebooking agent?
Make rebooking a tool call with consent on both sides: WHEN the passenger accepts a new flight THEN MUST call @[rebook_flight] and UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight]. Fetch options with a separate search tool, keep compensation amounts out of the replies and point at the claims form, and let the booking system pass the disruption type and the delay in minutes as typed inputs, so a $IF chain decides the meal voucher and refund lines before the conversation starts.
Why does my agent rebook passengers without asking them?
Because an unconditional instruction like rebook passengers on the next available flight applies on every turn, including the first one, before anyone has been offered a choice. Written as a rule it becomes MUST call @[rebook_flight], and TypeGlish proves it contradicts a consent rule such as UNLESS the passenger accepts a new flight THEN NEVER call @[rebook_flight] with a blocking logic/contradiction. Give the obligation the same guard as the prohibition and the conflict disappears.
Should the delay threshold for a meal voucher be in the prompt?
Put the threshold in a deterministic $IF chain over a number the booking system computes, not in a sentence the model has to evaluate. The model cannot see the departure board, so if the delay is over 3 hours asks it to guess. As a chain, the compiler checks every case is handled: without an $ELSE arm, structure/non-exhaustive-switch names the uncovered passenger, here a delay of exactly 180 minutes.
Does Don't work as a rule in a TypeGlish prompt?
No. DO NOT is a modal, but the contraction Don't is not, so Don't promise compensation reads as prose: the rules table marks it ✗ invisible and it earns no enforceability. Write NEVER or DO NOT, and use a verb the checker can observe, such as NEVER state a compensation amount, rather than promise.
Field note

The pattern generalises past airlines. Every CX agent that can change something, a booking, an order, a plan, a delivery slot, eventually gets a brief with an imperative at the top (rebook them, reschedule the delivery, switch them to the new plan) and a consent rule at the bottom, written by somebody else after an incident. In prose those two lines can sit together for months, and the model settles the argument differently on different days. As rules against a declared tool they are the same action with opposite force, and the compiler refuses to build until someone decides. The consent pair is also the cheapest possible defence against the failure why your agent promises the refund instead of issuing it describes from the other direction: an agent that says it acted when it did not. Here, the agent can only act when the passenger said yes, and only the tool call counts as acting.

∿ washed up Sep 28, 2026 ∿