Open Itinerary
An open, token-efficient JSON format for travel itineraries, designed for AI agents to output and apps to consume.
The problem
Ask five AI agents to plan a road trip and you get five completely different JSON shapes, or worse, prose. There is no standard for what a "stop" is, how alternatives are represented, or how to hand an itinerary off from one app to another without writing a custom parser. Existing formats do not help: Schema.org's Trip is too abstract for real itineraries, iCalendar was not designed for planning, and GTFS is transit-specific. Open Itinerary aims to be for travel plans what iCalendar is for events: boring, useful, and open.
What it is
It is a data model for travel plans, with two equivalent serializations:
.oitinerary.kdl — a token-efficient KDL format designed for AI agents to output. 60-80% fewer tokens than JSON.
.oitinerary.json — JSON, validated by JSON Schema, MIME application/vnd.open-itinerary+json. Use this for app consumption and validation.
Convert between them with the CLI:
cd agent-format
bun run src/cli.ts to-json examples/sf-to-la.oitinerary.kdl # KDL → JSON
bun run src/cli.ts from-json trip.json # JSON → KDL
Agent format (KDL)
itinerary "SF to LA Road Trip" {
summary "A 3-day coastal road trip from San Francisco to Los Angeles via Highway 1."
tz "America/Los_Angeles"
cur "USD"
tags "road-trip, coastal, california"
generated_by "claude-sonnet-4"
created_at "2026-05-11T10:00:00Z"
stop "monterey" {
name "Monterey Bay Aquarium"
goal "See the sea otters and kelp forest"
cat "attraction"
addr "886 Cannery Row, Monterey, CA 93940"
dur min=1.5 max=2.5
cost amt=65
alt {
name "Monterey State Beach"
goal "Free alternative — walk the beach instead"
cat "nature"
}
}
route "sf-to-monterey" {
from "sf"
to "monterey"
mode "drive"
dur min=1.75 max=2.5
dist 180
}
day date="2026-06-15" {
item type="stop" ref="monterey"
flex pick=1 {
option type="stop" ref="beach"
option type="note" txt="Relax at the hotel pool"
}
}
}
JSON format
{
"$schema": "https://raw.githubusercontent.com/ThatXliner/open-itin/main/open-itin.schema.json",
"version": "0.2",
"name": "SF to LA Road Trip",
"summary": "A 3-day coastal road trip from San Francisco to Los Angeles via Highway 1.",
"tags": ["road-trip", "coastal", "california"],
"tz": "America/Los_Angeles",
"cur": "USD",
"stops": [
{
"id": "monterey",
"name": "Monterey Bay Aquarium",
"goal": "See the sea otters and kelp forest",
"cat": "attraction",
"addr": "886 Cannery Row, Monterey, CA 93940",
"dur": { "min": 1.5, "max": 2.5 },
"cost": { "amt": 65 },
"alts": [
{
"name": "Monterey State Beach",
"goal": "Free alternative — walk the beach instead",
"cat": "nature"
}
]
}
],
"routes": [
{
"id": "sf-to-monterey",
"from": "sf",
"to": "monterey",
"mode": "drive",
"dur": { "min": 1.75, "max": 2.5 },
"dist": 180
}
],
"days": [
{
"date": "2026-06-15",
"items": [
{ "type": "stop", "ref": "sf" },
{ "type": "route", "ref": "sf-to-monterey" },
{ "type": "stop", "ref": "monterey" },
{
"type": "flex",
"pick": 1,
"opts": [
{ "type": "stop", "ref": "beach" },
{ "type": "note", "txt": "Relax at the hotel pool" }
]
}
]
}
],
"generated_by": "claude-sonnet-4",
"created_at": "2026-05-11T10:00:00Z"
}
After running the geocoder, coordinates are added automatically:
{
"id": "monterey",
"name": "Monterey Bay Aquarium",
"addr": "886 Cannery Row, Monterey, CA 93940",
"coords": {
"lat": 36.6183,
"lng": -121.9017,
"source": "nominatim",
"geocoded_at": "2026-05-11T10:00:00Z"
}
}
Key design decisions
name is truth, coords is a cache. AI agents hallucinate coordinates; they will confidently emit a latitude and longitude that is in the right region but wrong by kilometers. The schema makes name (and optionally addr) the authoritative location identifier. Coordinates live in a coords sub-object that is always produced by a geocoder, never by an agent. If the name changes, discard coords and re-geocode.
Every stop has a goal. The single most important field. It answers why you're stopping, not just where. This forces AI agents to be explicit about intent and gives consuming apps a human-readable string that works without further parsing.