XCTSK XCTrack task format
v1 file · v2 code We-Fly

Interactive specification

The XCTrack task format, decoded.

One competition task, two encodings: a readable .xctsk JSON file that every comp tool imports, and a dense XCTSK: string small enough to fit in a QR code on a briefing screen. Same task, wildly different bytes — this page maps one onto the other and lets you decode both in the browser.

JSON, UTF-8 CLASSIC tasks WGS84 · FAI sphere Polyline-packed coordinates
01 Overview

What a task file carries

A competition task is an ordered list of turnpoints — each a WGS84 position with a radius in metres — plus the rules that turn that geometry into a race: where the speed section starts, when its gates open, where it ends, and what shape the goal is.

XCTrack publishes two encodings of exactly that. The .xctsk file (version: 1) is verbose, pretty-printed JSON — the download format every competition tool reads: XCTrack, SeeYou Navigator, Airscore, FS. The task code (version: 2) is the same task squeezed into a single line so it survives being printed as a QR code on a briefing board and scanned off a phone screen.

Only CLASSIC. The taskType field exists because XCTrack also defines waypoint-less types. Every competition task in the wild is CLASSIC, and it is the only type described here — or accepted by We-Fly.

The vocabulary

TermMeaning
TakeoffThe launch cylinder. Optional, and never scored — the leg to the start is flown, not counted.
SSSStart of Speed Section. The clock starts when you cross it, in the direction the task declares.
ESSEnd of Speed Section. The clock stops here; the goal may still be further on.
GoalThe final turnpoint. A cylinder, or a line perpendicular to the inbound leg.
TurnpointAny other cylinder you must tag, in order.

Roles are positional as well as declared: whatever its type, the last turnpoint in the array is the goal. There is no GOAL type — the goal's shape lives in a separate goal block.

02 Two encodings

File, code, and how to tell them apart

Three byte strings can arrive at your parser. Dispatch on the first characters, not the file extension:

Starts withIsPayload
{.xctsk file — version 1Plain JSON object
XCTSK:Task code — version 2Plain JSON after the prefix
XCTSKZ:Task code — version 2, compressedBase64 of a raw zlib deflate stream
XCTSKZ: base64 is URL-safe-tolerant. Codes travelling through URLs or chat apps come back with - and _ in place of + and /. Translate them back before decoding, or a perfectly good code fails to inflate.

The two versions are not interchangeable inside their container: a version-1 body in an XCTSK: code, or a version-2 body in a .xctsk file, is malformed. Check the version field after parsing, and reject the mismatch rather than guessing.

03 v1 — the file

.xctsk · the readable form

chabre.xctsk — a five-turnpoint race to goal
{
  "taskType": "CLASSIC",
  "version": 1,
  "earthModel": "WGS84",
  "turnpoints": [
    {
      "type": "TAKEOFF",
      "radius": 400,
      "waypoint": {
        "name": "D01", "description": "Chabre launch",
        "lat": 44.3806, "lon": 5.7331, "altSmoothed": 1220
      }
    },
    {
      "type": "SSS",
      "radius": 4000,
      "waypoint": { "name": "B23", "lat": 44.4012, "lon": 5.7688, "altSmoothed": 980 }
    },
    {
      "radius": 2000,
      "waypoint": { "name": "A15", "lat": 44.5210, "lon": 5.9104, "altSmoothed": 1450 }
    },
    {
      "type": "ESS",
      "radius": 1000,
      "waypoint": { "name": "A07", "lat": 44.4455, "lon": 5.8102, "altSmoothed": 760 }
    },
    {
      "radius": 400,
      "waypoint": {
        "name": "G01", "description": "Goal field",
        "lat": 44.4301, "lon": 5.7955, "altSmoothed": 640
      }
    }
  ],
  "sss": { "type": "RACE", "direction": "EXIT", "timeGates": ["12:30:00Z", "12:45:00Z"] },
  "goal": { "type": "LINE", "deadline": "18:00:00Z" }
}

Read it as a route: launch at D01, start the clock leaving the 4 km B23 cylinder at 12:30, round A15, stop the clock at A07, then cross the goal line at G01. 1 018 bytes as laid out here. §04 is the same task in 350 characters.

Top level

KeyTypeMeaning
taskTypestringAlways "CLASSIC".
versionnumberAlways 1 in a file.
earthModelstring"WGS84" (default) or "FAI_SPHERE" — which geodesy the scorer uses for distances.
turnpointsarrayOrdered; the last entry is the goal.
sssobjectSpeed-section rules. Absent on an open-distance task.
goalobjectGoal shape and deadline.
oobjectPresent as {"v":2} when the SeeYou observation-zone extension is in use. See §07.

A turnpoint

KeyTypeMeaning
typestringTAKEOFF · SSS · ESS. Omitted for a plain turnpoint and for the goal.
radiusnumberCylinder radius in metres, integer.
waypoint.namestringShort competition code (B23, D01188) — what the instrument shows.
waypoint.descriptionstringOptional full name.
waypoint.lat / lonnumberSigned decimal degrees, WGS84. Five decimals is the working precision (≈ 1 m).
waypoint.altSmoothednumberTerrain elevation at the point, metres. Advisory — scoring is horizontal.
oobjectObservation zone, when the extension is active. See §07.

sss and goal

KeyValuesMeaning
sss.typeRACE · ELAPSED-TIMERace to goal with common gates, or an individual clock.
sss.directionENTER · EXITWhich crossing of the start cylinder counts. Marked obsolete in the XCTrack spec, but still written by every tool — read it, and keep writing it.
sss.timeGatesarray of HH:MM:SSZStart gates, UTC. Several for a multi-gate race — and legally empty, see the note below.
goal.typeCYLINDER · LINEGoal shape. A line is drawn through the goal point, perpendicular to the inbound leg, of length 2 × radius.
goal.deadlineHH:MM:SSZTask deadline, UTC. Optional.
An empty timeGates is legal. The spec calls the field required, but XCTrack exports "timeGates": [] for a task whose start time has not been set yet — a task still being planned. Rejecting the empty array turns an ordinary half-finished task into a parse error. Validate that the field is an array; do not require a length.
Times are UTC times of day, never dates. "12:30:00Z" — no date component anywhere in the format. The flying day comes from context (the file name, the comp schedule, the flight it is matched against). A task that crosses midnight UTC is ambiguous in this format; nobody flies one.
04 v2 — the code

XCTSK: · the QR form

Version 2 is the same task with every key abbreviated to one letter, every enum turned into a small integer, and each turnpoint's four numbers packed into a single string. The result is short enough to scan.

the Chabre task from §08, as a v2 code
XCTSK:{"taskType":"CLASSIC","version":2,"t":[{"z":"{v~a@wa{mGgkA_X","n":"D01","d":"Chabre launch","t":1},{"z":"_veb@ob_nGg|@_yF","n":"B23","t":2},{"z":"_kac@govnGsyA_|B","n":"A15"},{"z":"wxmb@kwgnGon@o}@","n":"A07","t":3},{"z":"{|jb@cwdnG_g@_X","n":"G01","d":"Goal field"}],"s":{"g":["12:30:00Z","12:45:00Z"],"d":2,"t":1},"g":{"t":1,"d":"18:00:00Z"}}

Five turnpoints, two start gates and a goal line in 350 characters. Paste it into the decoder in §08 — it is the same task as the file in §03.

Key map

v2v1 equivalentEncoding
t (top level)turnpointsArray, same order.
eearthModel1 = FAI_SPHERE. Absent = WGS84.
ssssObject.
g (top level)goalObject.
t[].zlon, lat, altSmoothed, radiusFour ints, polyline-packed. See §05.
t[].nwaypoint.nameString.
t[].dwaypoint.descriptionString; may be "".
t[].ttype1 TAKEOFF · 2 SSS · 3 ESS. 0/absent = plain.
s.gsss.timeGatesArray of HH:MM:SSZnot abbreviated. May be empty, as in v1.
s.dsss.direction1 ENTER · 2 EXIT.
s.tsss.type1 RACE · 2 ELAPSED-TIME.
g.tgoal.type1 LINE · 2 CYLINDER.
g.dgoal.deadlineHH:MM:SSZ.
The t collision is real. At the top level t is the turnpoint array; inside a turnpoint it is the role integer; inside s it is the start type; inside g it is the goal shape. Four different meanings, one letter — scope every lookup to its own object.
Which prefix to emit. XCTSKZ: compresses, but a typical task already fits a scannable QR uncompressed, and XCTSK: can be produced anywhere — no zlib, no WASM, works client-side. Emit XCTSK:, accept both. Both scan identically in XCTrack and SeeYou Navigator.
05 The z field

Four numbers in one string

Each v2 turnpoint packs longitude, latitude, altitude and radius — in that order — into z. Each value is scaled to an integer and encoded independently with Google's signed polyline algorithm, and the four results are concatenated with no separator. The decoder recovers exactly four integers or the turnpoint is malformed.

#ValueScaleUnit
1Longitude× 100 000degrees → 1e-5°
2Latitude× 100 000degrees → 1e-5°
3Altitude× 1metres
4Radius× 1metres
Longitude first. The opposite of the v1 file, of every coordinate you read aloud, and of most of aviation. It is the one mistake that produces a task which parses cleanly and lands in the wrong hemisphere.

The algorithm, per value

  • Take the scaled signed integer.
  • Left-shift by one; if the original was negative, invert every bit (~(v << 1)). This moves the sign into bit 0.
  • Emit the value five bits at a time, least-significant chunk first.
  • Every chunk but the last is OR-ed with 0x20 as a continuation flag.
  • Add 63 to each chunk and write it as an ASCII character — landing in the printable range ?~.

Decoding runs it backwards: accumulate 5-bit chunks until one arrives without the continuation flag, then undo the zig-zag (v & 1 ? ~(v >>> 1) : v >>> 1). Because the chunks are self-delimiting, four values concatenate unambiguously.

Try it · decode a z field
…and back · lon, lat, alt, radius → z
06 Field explorer

Every field, both versions

Search by field name or meaning, or narrow by category. Each card shows the v1 key and its v2 counterpart.

07 Observation zones

The SeeYou o extension

Base XCTrack turnpoints are always cylinders. SeeYou Navigator added an optional per-turnpoint o object to describe other observation-zone shapes, and XCTrack reads it. It appears in both versions, and is announced by a top-level "o": {"v": 2} marker.

KeyTypeMeaning
a1numberSector half-angle in degrees. 180 = a full cylinder. 45 = the FAI 90° photo quadrant.
a2numberSecond half-angle, for compound zones.
a12numberFixed bearing (0–359°) the zone is oriented to. Its presence is what makes a line "fixed" rather than auto-oriented.
r1 / r2numberInner / outer radii, metres.
lnumber1 = this zone is a line, not a cylinder.

Reading it back

  • No o object, or a1: 180 → plain cylinder.
  • l: 1 with a12 → a line at that fixed bearing.
  • l: 1 without a12 → a line auto-oriented perpendicular to the inbound leg. This is the default in both SeeYou and XCTrack.
  • 0 < a1 < 180 → a sector. Tools that model only the FAI quadrant treat any such value as the 90° sector.
Write cylinders explicitly when the extension is on. Once you emit "o": {"v": 2}, SeeYou writes an o block on every turnpoint — {"a1": 180} for the plain ones. Matching that byte-for-byte is what makes a round trip through SeeYou Navigator lossless.
08 Task decoder

Paste a task, read it back

Drop in a .xctsk file, an XCTSK: code or an XCTSKZ: code. Everything is decoded in the page — polyline fields unpacked, roles resolved, gates listed. Nothing is uploaded.

Task source · editable
Decoded live
09 Parsing gotchas

Where task files go wrong

  • Longitude leads in z. lon, lat, alt, radius. Swapping the first two is silent — the task still parses.
  • The goal has no type. The last turnpoint is the goal; its shape comes from the top-level goal block. Do not look for a GOAL value in type.
  • No sss means no speed section — an open-distance or free task. Do not synthesise a start from the first turnpoint.
  • Absent e is WGS84, not "unknown". Only e: 1 selects the FAI sphere.
  • An empty gate array is a valid task, not a malformed one. "timeGates": [] (v1) and "g": [] (v2) both mean "the start time is not set yet". Check the type, not the length.
  • Times carry no date, and Z is optional on input. Normalise to HH:MM:SSZ and reject out-of-range components rather than letting 25:00:00 through.
  • XCTSKZ: is raw deflate, not gzip. No gzip header, no member framing — inflate it as a zlib stream, and translate URL-safe base64 first.
  • A z field must yield exactly four integers. Three or five means a truncated or corrupted scan; fail the turnpoint rather than padding it.
  • Cap what you accept. A task code is a scanned string from an unknown source: bound the input size and the inflated size before you parse. We-Fly caps both at 64 KB.
10 In We-Fly

What We-Fly reads and writes

We-Fly imports .xctsk files and pasted codes in the competition-task library and the XC planner, and exports both forms.

DirectionFormNotes
Import.xctsk v1 fileUp to 64 KB. CLASSIC only.
ImportXCTSK: / XCTSKZ: codePasted into the task dialog; inflated server-side.
Export.xctsk v1 filePretty-printed, 2-space indent.
ExportXCTSK: code + QRUncompressed, so it can be produced in the browser; downloadable as PNG.

Round trips are tested both ways against the other three task formats — see the competition-task formats guide for how .fsdb, .tsk and .cup map onto this same model.

11 Sources

What this reconciles