Open format

The Common Course Format

Every Xogogy course is saved in the Common Course Format (CCF): one plain JSON document plus your media files. The format is published here, with its JSON Schema, under open licences. Anyone may read it, write it or build tools for it, free.

At a glance

  • Name: Common Course Format (CCF), version 0.1. A course file says which version it uses in schemaVersion, such as "0.1.0".
  • Schema: ccf-0.1.schema.json, a JSON Schema (draft 2020-12). Its identifier ($id) is urn:onenterframe:elearning-format-research:ccf:0.1.
  • Example: example-course.json, a complete two-slide course (shown below).
  • Licences: the schema under the MIT License, this guide under CC BY 4.0 (details).
  • In every export: “Export course data” writes ccf.schema.json, an exact copy of this schema, and a README that links back here. An export explains itself even without this website.

The project folder

A course is an ordinary folder. Nothing is hidden in a database or an account.

My course/
  My course.xogogy      the file you double-click to open the course
                        (older projects: My course.course)
  course.ccf.json       the whole course, as JSON
  assets/               images, audio, video, captions and fonts, as ordinary files
  history/
    log.jsonl           every change, one JSON object per line
    save-state.json     a small record used to detect an interrupted save
  fidelity-report.md    the import report, in plain language (imported courses)
  fidelity-report.json  the same report for tools (imported courses)
  • <Name>.xogogy marks the folder as a course so it opens with a double-click. It is a small JSON file that points at course.ccf.json; it holds no course content. Projects saved before the change carry <Name>.course instead, and open the same way.
  • course.ccf.json is the course. It is UTF-8 JSON, formatted the same way on every save, so it compares cleanly in Git or any diff tool.
  • assets/ holds each file the course uses. The course refers to them by path, such as assets/desk.png.
  • history/ is described below. save-state.json is bookkeeping (file hashes) and is safe to ignore.

The course file

A course needs only six fields: schemaVersion, id, title, source, canvas and slides. Everything else is optional, and absent means unknown: the format declares no defaults, and Xogogy never writes a value you did not choose. A player fills in its own defaults when it plays the course.

PartFieldsWhat it holds
Courseid, title, description, language, locales, textDirection, metadata, canvas, player, tracking, startSlideIdWho the course is, its main language, its slide size and layout model, player settings, what it reports to a learning system, and the slide it opens on.
SectionssectionsOrdered groups of slides (chapters, modules), each listing its slides by id.
SlidesslidesThe slides in presentation order, each with its objects, layers, triggers, timeline, notes and background.
VariablesvariablesNamed values the course reads and changes while it plays.
Questions and quizzesquestions, questionPools, quizzesEvery question with its choices, answer key and scoring; groups of questions drawn at random; quiz rules such as the passing score.
AssetsassetsThe media and font files the course uses.
Look and reusetheme, master, layouts, textStyles, objectStyles, defaultFormatting, actionGroups, interfaceSettingsShared colours and fonts, the slide master, slide layouts, named styles, formatting for newly created objects, reusable lists of actions, and permissions for embedded web content.
Losses and leftoversomissions, vendorExtensionsWhat an import could not carry over, and the original tool's data the format has no field for.
Source infosourceWhere the course came from: made in the app, or imported from another tool's project or published package.

Each part

Course and canvas

canvas.layout is fixed (one slide size), responsive (one slide size with breakpoint changes) or flow (no fixed size; content stacks and reflows). Fixed and responsive canvases give width and height in pixels.

Sections

Each section has an id, a title and slideIds, the slides it contains in order.

Slides

Each slide has an id, a kind (content, question or results) and a layout: fixed slides place objects at positions on the canvas; blocks slides stack content blocks from top to bottom. A slide may also carry layers, triggers, notes (speaker notes), a background, a transition, slide audio and cuePoints. A question slide names its question with questionId.

Objects

Each object has an id and a type: text, image, vector, shape, button, video, audio, webObject, table, textEntry, slider, dial, checkbox, radioButton, dropdown, group, widget or other. bounds gives x, y, width and height in canvas pixels from the top left corner. Text is in text.plain, with optional formatted paragraphs beside it. Objects can also carry timing, animations, accessibility text and child objects (for groups).

{
  "id": "o-desk",
  "name": "Desk photo",
  "type": "image",
  "bounds": { "x": 60, "y": 140, "width": 480, "height": 320 },
  "image": { "assetId": "a-desk" }
}

Layers

A layer is a set of objects shown over the slide, such as a pop-up tip or feedback. Each has an id and a kind (layer, feedback or panel), its own objects and triggers, and settings such as initiallyVisible, hideOtherLayers and pauseBaseTimeline.

"layers": [
  {
    "id": "l-tip",
    "name": "Tip",
    "kind": "layer",
    "initiallyVisible": false,
    "objects": [
      {
        "id": "o-tip-text",
        "name": "Tip text",
        "type": "text",
        "bounds": { "x": 600, "y": 220, "width": 560, "height": 120 },
        "text": { "plain": "Raise the screen to eye level." }
      }
    ]
  }
]

States

An object's states are named versions of it (for example Normal and Visited). A state changes only what it lists; everything else comes from the object. initialState names the state shown first, and state names are unique within an object.

"states": [{ "name": "Normal" }, { "name": "Visited" }],
"initialState": "Normal"

Triggers and actions

A trigger is an event plus an ordered list of actions. Events include click, timelineStart, timelineEnd, variableChanged, stateChanged, keyPress, dropOn, mediaEnded and questionSubmitted. Actions include goToSlide, nextSlide, showLayer, hideLayer, setState, setVariable, playMedia, submitQuiz and openUrl. Conditions are written only with an if action, which checks its condition once and runs then or else.

{
  "id": "t-tip",
  "event": { "type": "click", "objectId": "o-tip" },
  "actions": [
    { "type": "showLayer", "layerId": "l-tip" },
    { "type": "setState", "objectId": "o-tip", "state": "Visited" },
    {
      "type": "if",
      "condition": { "left": { "variableId": "v-visited" }, "operator": "eq", "right": { "literal": false } },
      "then": [{ "type": "setVariable", "variableId": "v-visited", "operator": "assign", "value": { "literal": true } }]
    }
  ]
}

The schema lists every event and action type with its fields.

Variables

Each variable has an id, a name, a type (text, number or boolean) and a kind: user for one you made, with its starting value in defaultValue; the other kinds (system, quizProperty, random, elapsedTime, questionResult) are values the player provides.

{ "id": "v-visited", "name": "Visited", "type": "boolean", "kind": "user", "defaultValue": false }

Questions and quizzes

Questions live in questions and slides point to them by id. There are thirteen types, from multiple_choice and true_false to matching, sequence, hotspot, drag and drop, Likert and essay. Each question has a prompt, scoring and an answer key, and says where the key came from in answerKeyStatus: declared (stated by the source), inferred (worked out from indirect evidence) or unresolved (not recovered, so no key is written). A quiz groups questions and sets one passing rule, a percentage or points.

"questions": [
  {
    "id": "q-height",
    "type": "multiple_choice",
    "prompt": { "plain": "Where should the top of the screen be?" },
    "scoring": { "mode": "graded", "correctPoints": 10 },
    "answerKeyStatus": "declared",
    "choices": [
      { "id": "c-eye", "content": { "plain": "At eye level" } },
      { "id": "c-low", "content": { "plain": "Below the chin" } }
    ],
    "correctChoiceId": "c-eye"
  }
],
"quizzes": [
  { "id": "quiz-main", "title": "Desk check", "questionIds": ["q-height"], "passingScore": { "percent": 80 } }
]

Assets

Each asset has an id, a type (image, vector, audio, video, font, document, captions or other) and a uri, usually a path under assets/. It may also record the file's media type, size, SHA-256 hash, pixel size, duration and alt text. Objects refer to assets by id.

{ "id": "a-desk", "type": "image", "uri": "assets/desk.png", "mimeType": "image/png", "altText": "A tidy desk by a window" }

Theme

The theme holds named colour slots (dark1, light1, dark2, light2, accent1 to accent6 and more), heading and body fonts, and optional background and object styles. Objects can use a theme colour by slot, so changing the theme changes them all.

"theme": {
  "name": "Calm",
  "colors": { "dark1": "#1F2933", "light1": "#FFFFFF", "accent1": "#2563EB" },
  "fonts": { "major": "Inter", "minor": "Inter" }
}

Omissions and vendorExtensions

Nothing is lost silently. When an import cannot carry something over faithfully, it adds an entry to omissions: a reason (unsupported, unmapped, ambiguous, unverified, invalidSource or lossy), an optional aspect (content, layout, styling, media and so on), a plain-language message, and a path, a JSON Pointer to the place in the course it concerns. Data from the original tool that the format has no field for is kept, as it was, under vendorExtensions, keyed by tool. A tool that edits the course should keep both, and treat vendorExtensions as possibly out of date.

"omissions": [
  {
    "path": "/slides/0/objects/1",
    "reason": "lossy",
    "aspect": "styling",
    "message": "The button's gradient fill was simplified to a solid colour."
  }
]

Source info

source.format says where the course came from: native for a course made in Xogogy, or the authoring tool or package kind for an imported one. source.artifact says what was read: sourceProject (an authoring project file), publishedPackage (a published LMS package), webExport or synthetic. Optional fields record the file name, its hash, the importer and when it was imported.

Stable ids

  • Every slide, object, layer, trigger, variable, question, choice, asset and section has an id: letters, digits and _ . : -, starting with a letter or digit, up to 128 characters.
  • An id is unique across the whole document, not just within its slide.
  • Parts refer to each other by id, never by position, so reordering slides breaks nothing.
  • Ids do not change when you edit. An imported course gets the same ids every time the same file is imported, and a new item gets the next free number for its kind, so the same edits give the same document.
  • The original tool's own identifier, when there is one, is kept separately in sourceRef.

How history works

history/log.jsonl holds one line per change, in order. A change (a batch) is everything one action or request did, applied all at once. Each line is a JSON object with:

  • t, the time; author, who made it (you, a command, or the name of an AI agent); and label, such as “Claude: added lesson 4”;
  • ops, the operations, each of kind set, insert, remove or move, naming the item it changes by id; a set records the value before and after.

Replaying the log on the course as it started gives the course as it is now, and undo works across sessions because the log is saved with the course. Undo steps back through the batches; a change you undid and did not redo is not kept when the course is saved. Every batch is checked against the schema before it is applied, and a batch that would leave an invalid course is refused with nothing changed. The log is plain text, so you can read it in any editor.

A complete example

Two slides, a title, a button, and a trigger that takes the learner from the first slide to the second. This is a whole, valid course file: it passes the schema and the app's extra checks, and a test in the app repository keeps it that way. Download example-course.json.

{
  "schemaVersion": "0.1.0",
  "id": "example-first-course",
  "title": "My first course",
  "language": "en-US",
  "source": { "format": "synthetic", "artifact": "synthetic" },
  "canvas": { "layout": "fixed", "width": 1280, "height": 720 },
  "startSlideId": "s-welcome",
  "sections": [
    { "id": "sec-main", "title": "Main", "slideIds": ["s-welcome", "s-done"] }
  ],
  "slides": [
    {
      "id": "s-welcome",
      "title": "Welcome",
      "kind": "content",
      "layout": "fixed",
      "objects": [
        {
          "id": "o-title",
          "name": "Title",
          "type": "text",
          "bounds": { "x": 60, "y": 40, "width": 1160, "height": 80 },
          "text": { "plain": "Welcome to the course" }
        },
        {
          "id": "o-next",
          "name": "Next button",
          "type": "button",
          "bounds": { "x": 60, "y": 340, "width": 220, "height": 52 },
          "text": { "plain": "Continue" }
        }
      ],
      "triggers": [
        {
          "id": "t-next",
          "event": { "type": "click", "objectId": "o-next" },
          "actions": [{ "type": "goToSlide", "slideId": "s-done" }]
        }
      ]
    },
    {
      "id": "s-done",
      "title": "Done",
      "kind": "content",
      "layout": "fixed",
      "objects": [
        {
          "id": "o-thanks",
          "name": "Thanks",
          "type": "text",
          "bounds": { "x": 60, "y": 40, "width": 1160, "height": 80 },
          "text": { "plain": "Thank you for taking the course." }
        }
      ]
    }
  ]
}

Read and write it with any JSON tool

The course is plain JSON, so any language or tool that reads JSON can open it. For example, list every slide title:

# jq
jq -r '.slides[].title' course.ccf.json

# Python
import json
course = json.load(open("course.ccf.json", encoding="utf-8"))
print([slide.get("title") for slide in course["slides"]])

# JavaScript (Node)
const course = JSON.parse(require("fs").readFileSync("course.ccf.json", "utf8"));
console.log(course.slides.map((slide) => slide.title));

To check a file, use any JSON Schema validator that supports draft 2020-12 with the schema. The app also runs checks a schema cannot express, such as that every id a trigger names exists and that no id is used twice; run xogogy validate <project folder> to apply both.

When you write or change a course file yourself:

  • Keep schemaVersion, and keep every id unique.
  • Refer to things by id, and update the references when you remove something.
  • Leave out what you do not know. Absent means unknown; do not fill in default values.
  • Keep omissions and vendorExtensions as they are unless you mean to change them.
  • Put media under assets/ and list each file in assets.
  • Validate before you open it in the app. Undo in the app covers the changes made through the app.

The versioning promise

  • The version is in every file (schemaVersion). The format is at version 0.1; fields may be added within 0.1.x, and a file that was valid stays valid.
  • A change that would make an existing file invalid gets a new version number, is announced, and has its own schema file here.
  • Old versions stay readable. Later versions of Xogogy open files written under earlier versions, and every published schema stays at its address, so a file can always be checked against the version it was written for.

Licences

See also Your files are yours.