Skip to content

Format reference

Format history

One entry per version bump of the .gigchart file, newest first. Every version is fully backward-compatible: the app still opens a file written at any earlier version, migrating it on load.

On this page

This is the log, not the spec. If you are writing a file today, write version 12 and read the format reference — it documents the current shape, key by key, with worked examples. Come here to understand a file you were handed, or a spelling you met in an older export.

Older files never need converting: the app migrates on load, and every legacy spelling below is still read. What a version bump signals is the other direction — an older build reading a newer file may not find everything in it.

v12 — 2026-07-28 — Settings lose their redundant prefixes

  • Every settings group already said what it was about, and then repeated itself in nearly every member — capo: { showCapo, capoOffset, capoPos, … }, ten *Prefix keys in labels, eight pageTurn* keys in page. Inside a group called capo, offset is the capo's offset, so each member is now spelled short.
  • Two families that had outgrown their group get containers: page.pageTurn (the eight band keys) and songInfoBox.fields (the per-field visibility flags).
  • header splits into title, subtitle and meta containers. The title/subtitle prefix only ever existed to tell the two apart inside one flat group; now each is show/align/color/offset, and the pair reads as the parallel thing it is.
  • The five video* keys move out of songInfoBox into their own video group — the video link is page chrome of its own, a peer of the capo band, and was never one of the box's rows.
  • songInfoBox.infoRows is now songInfoBox.rows, and page.pagePreset is page.preset.
  • Older files load unchanged, with no version check. Inside a group, a key the group knows is translated and anything else falls through as the legacy flat name — so capo: { showCapo } (v4–v11) and capo: { show } (v12) both load through one path, and a group you half-edit by hand loads correctly either way. The decision is per key, never per file.
  • Writes are a clean cut: the app writes only the new names. An older build reading a v12 file would not find its settings, which is what this bump signals.
  • This is a serialization change only — nothing about how a chart renders moved.
  • Additive since the bump. The entries below landed under v12 without a further bump: each is a new optional key, or a rename whose old spelling is still read, so an older build reading such a file gets less rather than wrong — and a bump is for the second case.
  • 2026-07-29 — annotation z, and the chord chip. An annotation may carry z, an integer paint depth (higher paints on top; absent = the kind's default, cues 1 and everything else 0). settings.concert gains the chip look: pillPad, pillPadV, pillBorder, chordFill, chordBorder, chordText, sepFill. A chordFont existed for one day, 2026-07-29 to 07-30; a file carrying it still loads, with the key ignored.
  • 2026-07-30 — video.scale becomes video.rows, and it is a different quantity, not a new name for the same one. scale was a multiplier on a fixed 16 mm square: it said how big the QR is in millimetres, which is true at the moment of the drag and wrong one font step later. rows is the square's side counted in body grid rows, so the code holds its proportion to the music at every size. scale is still read and converted against the file's own font size and line spacing — an older file opens at the size it printed at — but nothing writes it any more. If you are hand-writing a file, write rows.
  • 2026-07-30 — envelope pageCount. The laid-out page count as the writer last rendered the song. Derived provenance beside savedAt, not song content; readers treat it as a hint.
  • 2026-07-31 — settings.bottomBox is renamed settings.stamp. Same members. A stamp is defined by being applied, not by where it sits, and this one had been draggable to any corner for a long time, so "bottom box" had stopped describing it. The old group name is read as an alias, so a file written under it loads unchanged and cures its own spelling on the next save.
  • 2026-07-31 — scope, on six pieces of chrome. header.scope, songInfoBox.scope, capo.scope, video.scope, voiceLegend.scope and stamp.scope say which pages that element prints on: first · every · rest, plus last for the four that reserve no layout room. Before this there were two hardcoded answers in the whole app — everything in the header appeared on page 1, and the stamp appeared everywhere. Absent = the previous behaviour (first, or every for the stamp), and a file written before songInfoBox.scope existed inherits header.scope, which is what governed the box then. See the header group for why last is withheld from the two that reserve room.
  • 2026-07-31 — the video widget's own controls. video.logo (the app mark punched into the QR), video.shortUrl (print the link's short form), and video.sizeMode / sizeOffset / sizePct / sizePt — the text link's font size, the same four-mode chooser the header lines use, resolved against the annotation font. A file written before them loads onto defaults that reproduce the size it printed at.
  • 2026-07-31 — the placed qr annotation gains the same controls. linkMode 'qr' | 'link' (absent = 'qr', so every code placed before this stays a code), plus prefix, shortUrl and the shared boxSizeMode / boxSizeOffset / boxSizePct / boxFontPt four. The logo and the colors stay song-level and theme-level respectively — not per code.
  • 2026-07-31 — properties grows identifiers and video provenance. mbArrangerUri, mbInstrumentUri, mbGenreUri; regionUri, holidayUri, languageUri (Wikidata items, named for the field rather than the authority); and videoTitle / videoAuthor / videoAuthorUrl / videoFetched — what the video link resolved to when it was last looked up, and when.
  • 2026-08-11 — settings.geom.chrome, and geom.frame changes unit. The song chrome gains the same two drawing tokens an annotation carries, one entry per widget (info · capo · video · voices · stamp · concert · turn), so roundness and border width mean one thing whether you are editing a placed box or the footer badge. At the same time geom.frame stopped being an absolute px width and became a fraction of one text row like geom.radius, 0–0.25: the default hairline is unchanged because leaving the key out still means the chart-wide 1px, but a border you dial up now scales with the page as the font grows. frame was authorable in px for about twelve hours on 2026-08-10; a file carrying one of those values is converted on read at the default row, so it draws the same weight it did.
  • 2026-08-12 — geom.frameColor, and every box can be framed. A width with no colour was half a control: dialling a border up gave you a thicker version of an ink you could not choose. frameColor sits beside frame in the same bag — a hex, or transparent / currentColor — and is a slot reference like every other stored colour, so a chart re-themed later keeps its frames in step. In the same change, frame stopped being honoured by only the four kinds that happened to draw a border: every kind with a box now draws a default-zero frame, so a frame on a cue chip, a section spine or a capo pill means what it says. Both are additive — a file written before this carries neither key, and absent draws exactly what it always drew.
  • 2026-08-12 — a page pin gains a sheet and an opinion about the grid. Two additive keys beside pagePos. pinPage is the sheet the pin is frozen to: a pin used to freeze a coordinate but not a page, so a chip pinned to the bottom of page 2 followed its stanza to page 1 the moment the font shrank and arrived at the same x/y — neither position it had ever been placed at. pinFree settles the other half. While snapGrid is on, a pin is now re-locked to the nearest character-lattice point, because the lattice moves underneath a frozen coordinate — its origin follows the first body row, its cell is re-measured at every zoom — and a spot that was on a column drifts off one. That correction assumes the spot was chosen on the grid, which is true of every position the app snapped and false of one dragged with snapping off; pinFree is how such a position says so and is drawn verbatim. Both are absent in every file written before this, and absent means exactly what those files always did.
  • 2026-08-12 — the palette grows to 20 and is renumbered (v: 3). Five colors the set had no answer for — a brown, a dark green, a navy, a pink and a second gray — and, with them, a renumbering so that a swatch's index, its position in the row and the number printed on it are the same number. Slots were briefly appended past the rainbow to avoid moving anything, which left the file format's numbers disagreeing with the ones on screen; for anyone writing a chart by hand or generating one, a spec that says "swatch index" and means something other than the sixth circle in the row is a trap that never stops costing, so the indices moved instead. Existing charts are migrated on read and do not change appearance: every stored assignment is rewritten to the slot now holding the color it used to name, and the rainbow gradient samples the same ten hues it always did. The palette carries "v": 3; write that yourself if you hand-author a palette block, or it will be read as the older layout and migrated. Whole-library backups move to version: 3 for the same reason — they carry raw slot numbers in paletteAssign.
  • 2026-08-12 — a border is an ordinary palette color. The -1 sentinel introduced earlier the same day — derive this frame from the fill it rings — is retired. It needed a swatch in the color picker that was not one of the palette's colors, which is a thing to explain rather than a thing to look at. Frames now take an ordinary slot like any other element: the dark neighbour of the fill each one rings — red is framed in brown, green in forest, grey in dark grey. Nothing changes on screen: a frame's default width is zero. -1 is still READ — any out-of-range index resolves to that element's own default, so files carrying it keep working — but it should not be written, and no version was bumped for it.
  • 2026-08-12 — geom.pad, because the frame stopped pushing anything. A text box's border used to be a real CSS border, which came out of the box's inside and shoved the text in with it — visibly, and by a whole pixel even at width zero. It is now drawn inward, over the box, so it takes no room at any thickness and the words sit exactly where they sit with no border at all. That is the right default and it removes the only way there was to get the text off a heavy frame, so pad is that way: an inset, 0–0.5, in the same fraction-of-a-text-row unit as the two lengths beside it, and it is honoured on a textbox only. It takes its room out of the box rather than growing it, so the frame stays exactly the size it was and a line fits fewer characters — the honest price, and the one you are choosing. Additive: absent means the zero inset every chart written before this already had.
  • 2026-08-12 — geom.frameColor is ignored on a textbox. Not a format change, a correction to what this page claimed: the key was documented as applying to every kind that paints a box, and on a text box it never did anything. A text box colours its frame with boxStroke, which is also what says the box has a frame — so frameColor was a second control at the same tier writing a value nothing read. boxStroke keeps the job. Every other framed kind still reads geom.frameColor as its one per-widget override, and the frame WIDTH is unaffected on all of them, text box included: boxStroke says nothing about how thick the line is.
  • 2026-08-14 — settings.annotations.snapAlign. The alignment half of snapping, and until now the invisible half. Beside snapGrid, which says what LATTICE a drag steps on, this says whether a drag also snaps to things: the other widgets' edges and centres, and the page's own alignments — left margin, right margin, band centre — which a drop adopts outright, dropping the widget's nudge and recomputing its spot from the content box from then on. The adopt window is one whole grid column, which makes the position a single character off an alignment unreachable while it is on; that is deliberate, but it was happening with nothing drawn on screen, so it read as the drag refusing to go where it was put. The app now draws the guide line, and this key is the off switch. Absent = true, which is what every chart written before this was authored under. It is an authoring aid — it changes where a gesture lands, never where a saved widget prints — so a reader may ignore it and still render the file exactly.
  • 2026-08-14 — the two bar lists gain barRound, and the road map gains rowGap. roadmap and markers are the same list drawn on two axes, and they had drifted into two different widgets. barRound 'outer' | 'all' says where the corner radius lands — on the outside corners of the whole block, or on all four of every bar — and is read by both. Absent = 'outer', which is what a road map already drew; a marker group rounded every pill before this, so an older file reopens with the block look. That is a deliberate change of appearance, and a small one: the radius itself is untouched, in geom. rowGap 0 | 1 | 2 is blank body rows between a road map's stacked bars — the vertical twin of a marker's spacing, which counts characters between side-by-side ones. Two keys and not one because the units differ by axis, and because a markers list placed in a gap renders with the road map's stacked markup: reading its spacing (default 1) as rows would double the height of every gap marker ever written. In the same change, a road-map bar's label may contain newlines, each opening another body row of that bar, which the hosting spacer reserves. Both keys are absent in every file written before this, and absent means what those files already did.
  • 2026-08-15 — a horizontal marker group gains groupCols, and its automatic width now asks the page. A markers group set to textDir: 'normal' reads across rather than down, so its labels' LENGTH becomes its width — and that width is page room, reserved beside every stanza the group sits on. It was therefore bounded, at a flat 20 columns shared equally between the bars. A constant is the wrong bound here: 20 columns is a reasonable share of a Letter page and a third of a tablet spread, so a two-word group could wrap mid-word with half the sheet standing empty beside it. The bound is now the page's own figure — a group may claim up to half the body's columns, and it asks for what its labels need rather than for whatever is going spare, so the text always keeps the other half and the width still holds still across a font change. Within that, the columns are shared by need and not evenly: a short label is served in full and the long ones split what is left, instead of every bar being cut to the same width for the sake of the widest. groupCols is the manual override — the whole rack's width in grid columns, 440, the group-scale twin of a repeat bar's barCols. Narrower than the labels want and they wrap; wider and the extra is padding, which is how two groups on different stanzas are made to line up. Absent is a real value and means auto, so no existing file needs it — but auto is what changed, and a file whose labels wanted more than 20 columns, on a page with the room, now draws wider than it did.
  • 2026-08-12 — border color becomes a palette element, per kind. The palette's assign gains a …border entry for each of the nine boxed annotations (cueborder · sectionborder · repeatborder · capoborder · markersborder · roadmapborder · chorusborder · infoborder · chordboxborder), joining the frames the video widget, the voice legend and the page-turn band already had. Their default is not a hand-picked swatch but -1, a sentinel meaning derive this frame from the fill it goes around — a darker shade of that fill (lighter, once the fill is near black), which stays right when the fill is recolored, when the palette is swapped, and when one widget carries a color of its own. A per-widget geom.frameColor still overrides it. Additive and invisible until asked for: a frame's default width is zero, so a chart written before this draws exactly as much border as it did — none. Nothing shifted slot, so the palette's own version counter is unchanged.
  • 2026-08-16 — songInfoBox.color. The song info box's own fill, beside capo.color one group over: "" or absent inherits the colour theme's infobox swatch, a hex overrides it for this chart, and the text printed on it is chosen for contrast rather than stored. Like every other stored colour it is a slot reference that moves with the palette. The box already had this in its other home — the placeable info annotation carries a color on its own record — and the two are one widget, so the song-level box was the half that could not be recoloured without recolouring every chart on the theme. Additive: a file written before this carries no key, and absent draws the theme's swatch exactly as it always did.
  • 2026-08-16 — the video widget and the placed QR stop sharing a look. settings.video gains color and plateColor, and the placeable qr annotation gains qrLogo beside the color and boxFill it already had room for. They are the same widget in two homes, and until now the one thing they could not each own was how they LOOK: the editor’s colour rows wrote the theme ELEMENTS and its logo switch wrote the song-wide flag, so a pick made on a placed code repainted the song’s widget, every other placed code, and — since recolouring an element forks a factory palette — the look of every chart wearing it. Each widget now keeps its own, with the element as the DEFAULT: absent inherits qr/qrplate as a code and videolink/videolinkbg as a link, which is what every file written before this draws. The one reading that would otherwise change is the mark: absent now means ON, so a song that said logo: false has that answer stamped onto its placed codes as it loads. Recolouring every code at once is still one action — it is the element’s own door, in the colour manager.
  • 2026-08-17 — the two bar lists gain barFrame. barRound's question asked of the border rather than the corners, and read by both lists like its twin. A frame is drawn as an outline inside each painted element, and a road map's bars touch — so every internal edge of a framed stack carried two rings, one from each neighbour, and printed at twice the width of the frame around the outside. barFrame: 'outer' moves the ring onto the whole block, still inward, and leaves the bars unframed, so nothing is painted where two of them meet; 'all' is the per-bar look a framed list drew before this key. Absent = 'outer' where the bars touch and 'all' where rowGap/spacing holds them apart — and a gap overrides a stored 'outer', because bars that are visibly separate objects have no inside edge to skip. Additive, and it shows only on a list that sets geom.frame at all (default 0): such a file reopens with one ring instead of the doubled ladder, which is the point of the change.
  • 2026-08-17 — a spacer gains tailRows, and the song's edges may be zero. A boundary split by a page break renders none of its reserveLines: that room is padding above the lower stanza, and it is deliberately dropped when the stanza opens a page, because the break is already the bigger visual break. Useful as a default, but it left the foot of a page as the one place in a song with no spacing knob — the widgets standing in a split boundary were reserved for, the blank space around them was not. tailRows (0–12, default 0) is that knob: space at the foot of the page a boundary closes, live only while a break actually lands there, forced or automatic. It is the mirror of spaceAfter on a pagebreak (the top of the page a break opens) and lives on the spacer because the spacer is the only object an automatic break's boundary has. It is charged to the closing page, so a large value can push the upper stanza onto the next page. In the same change, reserveLines may be 0 at the song's edges — after: -1 and after the last stanza are margin, not separators — which is how a chart with no title starts flush at the top. Additive both ways: absent tailRows is exactly the old collapse, and nothing written before this moves. A spacer carrying tailRows is always written out, since the reader's boundary heal would not reconstruct it.

v11 — 2026-07-28 — Alignment and offset become exclusive

  • placement gains 'free' — shown as Follow text in the app. It is the only mode in which a widget's horizontal offset.dx is read, and its origin is the target's own left edge.
  • Every other placement is now an alignment: the app recomputes that spot from the content box and the widget's own width on each render, and ignores offset.dx. An aligned widget therefore cannot drift when the font size changes and cannot leave the page.
  • offset.dy is unchanged and honoured in every mode — vertically a widget is always follow-text, i.e. rows down from its target's top.
  • Up to v10 the two were superimposed: the app took a base from placement and added the scaled nudge on top, so a file could say placement:'left' with dx:+516.516 — "left-aligned, and 1250px to the right of left". The nudge scaled with the font while the page margin did not, so widgets walked off the page.
  • Older files are healed on load: the app works out where each widget actually drew, then either adopts the alignment cleanly (dropping the leftover nudge) or converts it to 'free' with dx rebased onto the target's left edge. A v2–v10 file opens unchanged; an older build reading a v11 file would mis-place a 'free' widget, which is what this bump signals.

v10 — 2026-07-28 — Linked data, second pass

  • The gc: vocabulary is published: https://www.gigcharts.app/ns/gigchart now resolves to the term definitions (Turtle alternate at /ns/gigchart.ttl). Until v10 that IRI fell through to the landing page — the terms looked resolvable and were opaque.
  • The envelope's id is now the JSON-LD node identifier: the context maps id@id. It was previously read as an ordinary string property, so a chart expanded to a blank node — nothing could refer to it.
  • The envelope gains @type (gc:Chart) and song gains @type (schema:MusicComposition). Both optional in a hand-written file — the app stamps them on save.
  • An annotation's kind is now its JSON-LD type: the context aliases kind@type, so a cue expands as gc:cue and its target as gc:line. No file change — the same JSON, better typed. The classes are defined in the vocabulary.
  • duration maps to gc:duration, not schema:duration: the file holds a display string (3:45) and schema:duration's range is an ISO-8601 duration (PT3M45S). Asserting one into the other was a range violation.
  • savedAt is typed xsd:dateTime rather than schema:DateTime — the RDF-native datatype.
  • Nothing in this version changes how a file loads. A v10 file opens in an older build (the added keys are ignored), and a v9 file opens unchanged.

v9 — 2026-07-28 — Semantic stanza anchors

  • New song.structure: one sticky id per stanza, in body order — the anchors an annotation's sid / gapAfterSid / refSid point at, so a target survives editing the body above it.
  • Annotation targets carry their stanza id beside the index; the index remains the fallback for a file without a ledger.
  • textRanges replaces flat-offset textStyles where every styled run has a stanza to name — stanza-local { sid, line, from, to }. A run with no stanza home (it sits on a blank line) still writes the flat form, and one file never carries both.
  • A target may say lineBasis: 'lyric' to count lyric lines only, and let the app resolve the rendered row.

v8 — 2026-07-28 — Interchange & FAIR pass

  • The envelope gains @context (the published JSON-LD context, below), an ISO-8601 savedAt, and a generator string — the build (and git sha) that wrote the file.
  • tempo / year / track write as JSON numbers when the value is cleanly numeric; a non-numeric value (e.g. "3 of 12") still writes as a string.
  • Every number in the file is rounded to 3 decimal places — drag/zoom offsets no longer carry float64 noise.
  • The song's own voice roster travels in full, not just the voices an annotation actually references.
  • The active color palette always travels, including the four factory palettes — previously only a custom palette was written.
  • New property: license — the chart/arrangement's own license, distinct from copyright.
  • The file's declared media type is now application/vnd.gigchart+json.
  • The envelope gains id — the chart's own identifier (urn:uuid:…), for provenance/dedup. It resolves nowhere and names nothing outside the file; opening a file never adopts its id — the app always mints a fresh library id on import.
  • Ids are now optional everywhere in a hand-written file: annotation ids, bar-item ids, info-row ids and spacer ids are minted on read when absent. The app itself no longer writes bar-item ids, info-row ids, or unreferenced spacer ids at all. Never invent an id — omit it.
  • spacers is now always present in what the app writes (possibly []): a boundary's lone default spacer (reserveLines 1, not hidden, referenced by no annotation) is omitted by the writer and reconstructed identically by the reader. Absence of the key entirely is still the pre-v5 migration signal.
  • An annotation offset of {dx:0, dy:0} is omitted — absence already means no nudge.

v7 — 2026-07-24 — Info box group renamed

  • settings.infoBox1 renamed to settings.songInfoBox — same keys. A pre-v7 file's infoBox1 group is still read as an alias.

v6 — 2026-07-23 — Format sanity-check cleanup

  • properties.revision replaces properties.version for the song's own revision — it no longer collides with the envelope's format version.
  • textStyles uses spelled-out keys (italic/underline/voice), not the old single letters (i/u/v).
  • The recall annotation kind serializes as recall (was chorus).
  • Every legacy spelling above is still read, so v2–v5 files load unchanged.

v5 — 2026-07-19 — Explicit spacers

  • Gap-scoped annotations (cue, road map, page break, repeat-section marks) are hosted by explicit spacer blocks that own the boundary and its height.
  • A pre-v5 file has no spacers key — the app migrates its { kind: 'gap' } annotations into spacers on load.

v4 — 2026-07-08 — Grouped settings, American spelling

  • settings is grouped by function (typography/header/songInfoBox/bottomBox/capo/notation/concert/annotations) instead of one flat ~50-key bag.
  • Colors serialize as color (American spelling), not the British colour. Reads still accept colour, so pre-v4 files keep loading.

v3 — 2026-07-04 — Markers unified

  • The performer kind (side-by-side single-label panels) became markers — one grouped element carrying an ordered list of colored bars. Old v2 performer annotations are not migrated; files still read back to v2.