Introduction
Here’s a fourth, production-ready pattern you can drop into your stack: a Corporate Travel Booking Agent. It reads a traveler’s request, checks company travel policy and budget, searches itineraries, holds the best compliant option, and—only with approval or within self-serve limits—purchases the ticket. It never claims success unless the backend executed the action and returned a receipt.
The Use Case
Employees ask for flights (origin, destination, dates, cabin, constraints). The agent should enforce policy (max fare, allowed cabins by route, preferred carriers), handle approvals for over-policy cases, and return a single held or purchased itinerary with a confirmation reference and clear next steps.
Prompt Contract (agent interface)
# file: contracts/travel_booking_v1.yaml
role: "TravelBookingAgent"
scope: >
Plan and book business travel within policy. Ask once for missing fields
(user_id, origin, destination, depart_date, return_date, cabin).
Prefer preferred carriers; enforce fare caps; propose tools and never assert
success unless a receipt is present.
output:
type: object
required: [summary, decision, itinerary, citations, next_steps, tool_proposals]
properties:
summary: {type: string, maxWords: 70}
decision: {type: string, enum: ["search_only","hold","purchase","need_approval","need_more_info"]}
itinerary:
type: object
required: [origin, destination, depart, return, carrier, cabin, price_cents]
properties:
origin: {type: string}
destination: {type: string}
depart: {type: string}
return: {type: string}
carrier: {type: string}
cabin: {type: string}
price_cents: {type: integer}
citations: {type: array, items: {type: string}}
next_steps: {type: array, items: {type: string}, maxItems: 6}
tool_proposals:
type: array
items:
type: object
required: [name, args, preconditions, idempotency_key]
properties:
name: {type: string, enum: [GetPolicy, SearchFlights, HoldItinerary, EnsureApproval, PurchaseTicket]}
args: {type: object}
preconditions: {type: string}
idempotency_key: {type: string}
policy_id: "travel_policy.v6.us"
citation_rule: "1–2 minimal-span claim_ids per factual sentence"
decoding:
narrative: {top_p: 0.92, temperature: 0.72, stop: ["\n\n## "]}
bullets: {top_p: 0.82, temperature: 0.45}
Example claims (context given to the model)
[
{"claim_id":"policy:farecap:domestic","text":"Domestic roundtrip fare cap is $700 all-in.",
"effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"$700 all-in"},
{"claim_id":"policy:cabin:domestic","text":"Domestic flights must be Economy; Premium Economy allowed if >4 hours.",
"effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"Economy; Premium Economy >4h"},
{"claim_id":"policy:preferred:carrier","text":"Preferred carriers are BlueSky and AeroLine.",
"effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"Preferred carriers"},
{"claim_id":"policy:approval:overcap","text":"Over-cap fares require manager approval.",
"effective_date":"2025-06-01","source_id":"doc:travel_policy_v6","span":"require manager approval"}
]
Tool Interfaces (typed, with receipts)
# tools.py
from pydantic import BaseModel
from datetime import date, datetime, timedelta
from typing import Optional, Dict, List
class GetPolicyArgs(BaseModel):
user_id: str
route_type: str # "domestic" | "international"
class SearchFlightsArgs(BaseModel):
origin: str
destination: str
depart_date: date
return_date: date
cabin: str # "economy" | "premium_economy" | "business"
preferred_carriers: Optional[List[str]] = None
max_results: int = 5
class HoldItineraryArgs(BaseModel):
user_id: str
carrier: str
price_cents: int
cabin: str
origin: str
destination: str
depart_iso: str
return_iso: str
hold_minutes: int = 24*60
class EnsureApprovalArgs(BaseModel):
user_id: str
manager_id: str
reason: str
price_cents: int
class PurchaseTicketArgs(BaseModel):
hold_ref: str
user_id: str
class ToolReceipt(BaseModel):
tool: str
ok: bool
ref: str
message: str = ""
data: Optional[Dict] = None
# adapters.py (demo logic)
from tools import *
from datetime import date, datetime, timedelta
PREFERRED = ["BlueSky","AeroLine"]
FARE_CAP_DOMESTIC = 70000 # cents
def get_policy(a: GetPolicyArgs) -> ToolReceipt:
return ToolReceipt(tool="GetPolicy", ok=True, ref=f"pol-{a.user_id}",
data={"fare_cap_cents": FARE_CAP_DOMESTIC if a.route_type=="domestic" else 300000,
"preferred": PREFERRED,
"cabin_rules": "economy; premium_economy if >4h domestic"})
def search_flights(a: SearchFlightsArgs) -> ToolReceipt:
# mock 2 results; in real life call GDS/OTA/airline API
results = [
{"carrier":"BlueSky","price_cents": 58000,"cabin": "economy",
"depart_iso": f"{a.depart_date}T09:10:00","return_iso": f"{a.return_date}T17:35:00",
"origin": a.origin, "destination": a.destination},
{"carrier":"AeroLine","price_cents": 74500,"cabin": "premium_economy",
"depart_iso": f"{a.depart_date}T08:00:00","return_iso": f"{a.return_date}T18:20:00",
"origin": a.origin, "destination": a.destination}
]
return ToolReceipt(tool="SearchFlights", ok=True, ref="search-rt", data={"results": results[:a.max_results]})
def hold_itinerary(a: HoldItineraryArgs) -> ToolReceipt:
expires_at = (datetime.utcnow() + timedelta(minutes=a.hold_minutes)).isoformat()
return ToolReceipt(tool="HoldItinerary", ok=True, ref=f"hold-{a.carrier}-{a.origin}{a.destination}",
data={"expires_at": expires_at, "price_cents": a.price_cents})
def ensure_approval(a: EnsureApprovalArgs) -> ToolReceipt:
# pretend manager approved; return false sometimes in real impl
approved = a.price_cents <= 90000 # approve if ≤ $900 for demo
return ToolReceipt(tool="EnsureApproval", ok=approved,
ref="appr-123" if approved else "appr-denied",
message="Approved" if approved else "Denied")
def purchase_ticket(a: PurchaseTicketArgs) -> ToolReceipt:
return ToolReceipt(tool="PurchaseTicket", ok=True, ref=f"tkt-{a.hold_ref}",
data={"ticket_number": "880-1234567890"})

Join the conversation! Your thoughts help the community grow.