Contrail

An aviation schema for every flight

Contrail is a simple set of lexicons for the AT Protocol to describe flights. It provides a common structure to support building compelling aviation-centric experiences on the ATmosphere.

To get started, view the definitions or try creating some records using the Flighty importer demo.

Introduction

Contrail is a proposal to provide lexicons to describe flight segments. A contrail record merely asserts that a particular flight existed, additional optional fields exist to provide context. For instance, a user's connection to a particular flight (i.e. as a passenger or pilot) is entirely optional. In fact, the only mandatory field required is createdAt. This is the most important property of the schema and everything else follows from it.

An absent field means the value is unknown or does not apply. It never means zero, empty, or none, and the schema defines no placeholder values. A record describing a flight where only the date and the tail number are known is as valid as one with every field populated. Tools that require complete data should enforce that requirement themselves, since the schema cannot do so without excluding general aviation, where most of the commercial fields have no meaning.

The relationship field is the one exception to this rule. When it is absent, no claim is being made about the author's involvement, rather than the claim being unknown. A record created from ADS-B data about another operator's aircraft simply omits it.

Records for every kind of flying

Contrail is intended to describe any kind of flying. Scheduled airline service and general aviation are the most familiar examples, and they sit at roughly opposite ends of how much structured information a flight carries, but they are not the limits of the schema. Cargo, charter, business aviation, air ambulance, law enforcement, government, military and flight training operations all use the same record type.

The difference between them is only which fields happen to apply. A cargo flight has an operator and a flight number but no seat or cabin. A police helicopter may have nothing beyond a registration, an aircraft type, and a takeoff and landing time. Both are complete records, and so are the two below.

Commercial
{
  "$type": "com.airplaneian.contrail.temp.flight",
  "createdAt": "2026-07-25T09:14:00-07:00",
  "date": "2025-07-13",
  "origin": { "iata": "SFO", "terminal": "INTL", "gate": "A2" },
  "destination": { "iata": "LHR", "terminal": "3" },
  "operator": "VIR",
  "flightNumber": "20",
  "marketingAirline": "DAL",
  "marketingFlightNumber": "5964",
  "registration": "GVBOW",
  "aircraftType": "Boeing 787-9",
  "actualGateDeparture": "2025-07-13T15:57:00-07:00",
  "actualTakeoff":       "2025-07-13T16:25:00-07:00",
  "actualLanding":       "2025-07-14T10:14:00+01:00",
  "actualGateArrival":   "2025-07-14T10:23:00+01:00",
  "relationship": "passenger",
  "seat": "46K",
  "status": "normal",
  "source": "flighty",
  "sourceId": "bef37d51-2876-433b-bffd-95c81f8fe569"
}
General aviation
{
  "$type": "com.airplaneian.contrail.temp.flight",
  "createdAt": "2026-07-25T09:14:00-07:00",
  "date": "2026-07-19",
  "origin": {
    "name": "Twin Oaks Airpark",
    "faaLid": "7S3",
    "geo": { "latitude": "45.42361", "longitude": "-122.94028" }
  },
  "destination": {
    "name": "Twin Oaks Airpark",
    "faaLid": "7S3",
    "geo": { "latitude": "45.42361", "longitude": "-122.94028" }
  },
  "registration": "N12345",
  "icao24": "a1b2c3",
  "icaoTypeDesignator": "PA18",
  "aircraftType": "Piper PA-18 Super Cub",
  "actualTakeoff": "2026-07-19T10:12:00-07:00",
  "actualLanding": "2026-07-19T10:58:00-07:00",
  "relationship": "pic",
  "source": "manual"
}

Note that the origin and destination in the general aviation example are the same place. Local flights are ordinary and the schema treats them as such.

Identifying flights across repositories

Because a record describes the flight rather than the author, several people aboard the same aircraft may each write a record for it. None of these is canonical, and Contrail treats that duplication as expected. It does not attempt to define a global flight identifier, because no authority exists to issue one.

This does mean readers need some way to recognise two repositories describing the same flight, so the identifying fields carry more weight here than they would in a personal logbook. Write them consistently wherever they are known.

Commercial date + operator + flightNumber
General aviation icao24 + actualTakeoff

The operator field takes the ICAO designator of whoever actually flew the aircraft, and is not restricted to airlines: UAL for an airline, FDX for a cargo carrier, RCH for a military air mobility flight. It is the ICAO designator rather than the IATA code (UA) because IATA codes are reassigned between carriers over time, which makes a historical record tagged with one ambiguous. Operators with no designator at all, which is most of general aviation, go in operatorName instead. Registrations are written in uppercase with no hyphens, so that G-VBOW and GVBOW do not describe two different aircraft.

Airports and landing sites

Every place in a record uses the same definition, whether it is an origin, a destination or a diversion, and all of its fields are optional. That is what lets one shape describe both a major international airport and a grass strip.

Three identifier fields exist because no single system covers everything. ICAO location indicators are globally unique and are not reassigned, so prefer icao where a place has one. IATA codes are reassigned between airports over time and are absent for the great majority of general aviation fields. Many United States airfields have an FAA Location Identifier and no ICAO indicator at all, which is why faaLid is its own field rather than something written into icao.

Never derive one identifier from another. A wrong code is worse than an absent one, and readers reconciling records match on whatever the fields actually contain. Sites that no identifier system covers, including private strips and off-airport landings, use name and geo instead.

Terminal and gate live here too. They describe how a particular flight was handled at a place rather than a property of the place itself, which is why they belong to the record and not to any airport dataset.

Working with surveillance data

The aircraft and operator fields are shaped to take ADS-B data without losing anything. A surveillance feed supplies a Mode S address, a registration, an ICAO type designator, a human-readable type description and a callsign, and each of those has a field of its own.

The callsign is written verbatim into callsign and is never decomposed on the way in. UAL38 and FDX5608 split cleanly into an operator and a flight number, so an importer can fill those as well. A general aviation aircraft transmits its registration, and a state aircraft may transmit a bare word such as LEGACY. Anything that cannot be split with certainty stays in callsign alone.

Surveillance datasets usually expose a single combined owner-or-operator field, and in practice its contents are the registrant rather than the operator. Much of the fleet is registered to trusts and single-purpose holding companies, so a Pilatus PC-12 comes back registered to a trust company and a Cessna 172 to an LLC named after its own tail number. That value belongs in registeredOwner. Military and state aircraft usually have no entry at all.

Grouping flights into trips

A second record type, com.airplaneian.contrail.temp.trip, groups flights that belonged to one journey. It is a record of its own rather than a field on the flight, so that a flight record stays a standalone assertion. Deleting a trip orphans nothing, and a flight written by someone with no interest in trips is unaffected.

The reference points one way only, from the trip to its flights. Nothing in the protocol keeps two directions consistent, so a flight carries no trip reference and there is no pair of fields that can disagree with each other.

Flights are referenced by plain AT-URI rather than by com.atproto.repo.strongRef. A strong reference pins exact record content, which is right for a like or a repost, but flight records are expected to be corrected in place as better data arrives and every correction would break a pinned reference. The tradeoff is that a reference may dangle if a flight is later deleted, and readers must tolerate that rather than treat it as an error.

A trip references only flight records its own author wrote, and makes no claim about anyone else's repository. The importer below derives trips from booking references, which are read in the browser and never written into a record, because a booking reference can be used to retrieve a reservation.

What Contrail leaves out

Duration, distance, and great circle mileage are all derivable from the times and places already present in a record. Storing them would guarantee that records written by different tools eventually disagree with each other.

Airport names, coordinates, and elevations are reference data about the world rather than assertions by the author, so they do not belong in a personal repository. The one exception is place.geo, which exists for landing sites that no identifier system covers.

Times and time zones

Contrail defines eight timestamps, following the standard out, off, on and in convention, each with a scheduled and an actual variant. All of them are optional. A general aviation record will often carry only a takeoff and a landing.

Every timestamp is written with an explicit UTC offset and no fractional seconds. The Lexicon specification notes two round tripping ambiguities for datetimes, one involving lost precision and one involving trailing fractional second zeros, and a fixed convention avoids both.


Flighty importer demo

The demo below imports a Flighty CSV export into your repository as Contrail records. It runs entirely in this page. Your file is parsed in the browser and is never uploaded, because there is no server to upload it to.

Before you write anything

A complete flight history is also a movement history. Once these records are in your repository they are public and permanent, and a reader can work out where you are based, how often you travel, and when you are away from home.

This holds regardless of what the records claim. The relationship field being optional does not change it, because records about flights sitting in your repository disclose your connection to those flights either way.

Definitions

The following schemas are published to did:plc:5jsivedaksbcc4n2epiegflk and resolve by NSID.

com.airplaneian.contrail.temp.flightrecord, key: tid

An assertion that a particular flight existed, rather than a personal log entry about the author. Only 'createdAt' is required. Every other field is optional, and absent means unknown or not applicable rather than empty or zero.

The exception is 'relationship', where absent means no claim is being made. Records of the same flight written by different people are expected, so write the identifying fields consistently. Any kind of flying is in scope, and origin may equal destination.

31 fields, 1 required

createdAtstring:datetimerequired

When this record was written, not when the flight happened. Explicit UTC offset, no fractional seconds.

datestring
maxLength: 10

Calendar date of departure at the origin, as 'YYYY-MM-DD'. Part of the identity key with 'operator' and 'flightNumber'. Must agree with any departure timestamp.

origincom.airplaneian.contrail.temp.defs#place

Where the flight departed. May equal 'destination'; local flights are ordinary.

destinationcom.airplaneian.contrail.temp.defs#place

Where the flight arrived, or was intended to arrive if it was cancelled. If it diverted, this stays the intended destination and 'diversionAirport' holds where it landed.

sourcestring
maxLength: 64

Short identifier for the tool or dataset that wrote this record, for example 'flighty', 'manual', or 'adsb'.

sourceIdstring
maxLength: 512

That tool's own identifier for this flight. Kept in the record body rather than the record key, so a record can be corrected without becoming a new one. Meaningful only with 'source'.

callsignstring
maxLength: 16

The flight identification the aircraft transmitted, uppercase, verbatim. Never decompose it: populate 'operator' and 'flightNumber' as well only when the split is unambiguous.

operatorstring
maxLength: 8

ICAO designator of the agency that operated the flight, three letters, uppercase. Not airlines only: 'UAL', 'FDX', 'RCH'. Use 'operatorName' where no designator exists.

operatorNamestring
maxLength: 128

Free-text name of the operating agency, for operators with no ICAO designator. Not reliable for matching. This is who flew the aircraft, which is often not who owns it.

flightNumberstring
maxLength: 16

The operating agency's flight number, as a string. Digits only, without the operator prefix.

marketingAirlinestring
maxLength: 8

ICAO designator of the airline that sold the flight, when it differs from 'operator'. Codeshares only.

marketingFlightNumberstring
maxLength: 16

Flight number under which the flight was sold, when it differs from 'flightNumber'.

registrationstring
maxLength: 16

Registration or tail number, uppercase with no hyphens or spaces, so that 'G-VBOW' and 'GVBOW' are one aircraft. Military and state identifiers go here too.

registeredOwnerstring
maxLength: 128

Free-text name of the party the airframe is registered to. Often a trust or holding company, and frequently not the operator.

aircraftTypestring
maxLength: 128

Free-text aircraft type as the source expressed it, for example 'Boeing 737-800'. Not reliable for matching. Do not derive it from 'icaoTypeDesignator' or the reverse.

icaoTypeDesignatorstring
maxLength: 8

ICAO aircraft type designator, for example 'B77W'. Write it only when the source supplies a designator.

icao24string
maxLength: 8

The airframe's 24-bit ICAO address, six lowercase hexadecimal characters. With an actual departure time this is the identity key for flights with no flight number.

scheduledGateDeparturestring:datetime

Scheduled 'out' time, leaving the gate or parking position. Explicit UTC offset, no fractional seconds.

actualGateDeparturestring:datetime

Actual 'out' time, leaving the gate or parking position. Explicit UTC offset, no fractional seconds.

scheduledTakeoffstring:datetime

Scheduled 'off' time, wheels off. Explicit UTC offset, no fractional seconds.

actualTakeoffstring:datetime

Actual 'off' time, wheels off. Explicit UTC offset, no fractional seconds.

scheduledLandingstring:datetime

Scheduled 'on' time, wheels on. Explicit UTC offset, no fractional seconds.

actualLandingstring:datetime

Actual 'on' time, wheels on. Explicit UTC offset, no fractional seconds.

scheduledGateArrivalstring:datetime

Scheduled 'in' time, reaching the gate or parking position. Explicit UTC offset, no fractional seconds.

actualGateArrivalstring:datetime

Actual 'in' time, reaching the gate or parking position. Explicit UTC offset, no fractional seconds.

relationshipstring
maxLength: 64
known valuespassengerpicsicstudentinstructorcrewobserver

The author's connection to this flight. Absent means no claim is being made, unlike every other field where absent means unknown. Open string: readers must tolerate values outside the known list.

seatstring
maxLength: 16

Seat occupied, as a string, for example '14C'.

cabinstring
maxLength: 64
known valueseconomypremiumEconomybusinessfirst

Cabin or class of service. Open string, lower camel case; readers must tolerate values outside the known list.

statusstring
maxLength: 32
known valuesnormalcancelleddiverted

How the flight concluded. Absent means unknown, so write 'normal' explicitly when the flight operated as planned.

diversionAirportcom.airplaneian.contrail.temp.defs#place

Where the flight actually landed, when that was not 'destination'. Present only when 'status' is 'diverted'.

notesstring
maxLength: 10000 maxGraphemes: 1000

Free-text note. Public and permanent once published; do not put booking references or other credentials here.

com.airplaneian.contrail.temp.triprecord, key: tid

An assertion that a set of flights belonged to one journey. Only 'createdAt' is required, and absent means unknown or not applicable. The reference points one way only, from trip to flight, and a trip references only flight records the author wrote.

5 fields, 1 required

createdAtstring:datetimerequired

When this record was written, not when the trip happened. Explicit UTC offset, no fractional seconds.

namestring
maxLength: 640 maxGraphemes: 64

Free-text name for the trip. Public and permanent once published.

flightsarray<string:at-uri>
maxLength: 200

The flights in this trip, in the order flown, as AT-URIs to flight records. Plain AT-URIs rather than strong references, since flight records are corrected in place. References may dangle; readers must tolerate that.

sourcestring
maxLength: 64

Short identifier for the tool that wrote this record.

sourceIdstring
maxLength: 512

That tool's own identifier for this trip. A grouping key may be sensitive even when the grouping is not: never write a booking reference here.

com.airplaneian.contrail.temp.defs#placeobject

An airport, airfield, or landing site referenced by a flight record. Every field is optional, and absent means unknown or not applicable. Write the identifier fields wherever they are known, since readers match on them, and prefer 'icao' where a place has one.

Never invent or derive an identifier the source did not supply.

7 fields, 0 required

icaostring
maxLength: 8

ICAO location indicator, four characters, uppercase, for example 'KOAK'. Preferred for matching. Many small airfields have none.

iatastring
maxLength: 8

IATA location code, three characters, uppercase, for example 'OAK'. Reassigned between airports over time, so pair it with 'icao' or 'geo' where possible.

faaLidstring
maxLength: 8

FAA Location Identifier, for United States airfields that have one, for example '1G5'. Many US fields have this and no ICAO indicator.

namestring
maxLength: 256

Free-text name, for landing sites that no identifier system covers. Carries no matching guarantees.

geocommunity.lexicon.location.geo

Coordinates, for landing sites that no identifier system covers. Do not write coordinates looked up from an identifier already in this object.

terminalstring
maxLength: 32

Terminal used by this flight at this place, as free text. A fact about the flight, not about the place.

gatestring
maxLength: 32

Gate or stand used by this flight at this place, as free text.

These are currently published under a temp namespace while the shape settles. Published lexicon constraints can never be loosened afterwards, so the stable names will not be claimed until real data has been through them.