keep club course docs
Guides and this page

Course format 2

Creating a course

Courses are plain YAML files. You can start with a couple of prompts, then add answers, sections, media, or other details if you need them.

Start small

The same format works for a two-card course and a large course. Add optional fields as you need them.

Readable field names

The public fields have names such as courseId, cardId, front, and back.

Course files contain content

Review history, due dates, and other learner data are stored by the app, not in the course file.

The smallest useful course

Create a UTF-8 file named course.keep.yml:

schemaVersion: 2
courseId: daily-prompts

cards:
  - cardId: plan-tomorrow
    front: Write down tomorrow's three priorities.

  - cardId: recall-the-day
    front: Recall three things you learned today.

This is a complete course with two front-only cards. It has no theme, sections, or title. keep club derives a display title from daily-prompts, creates one “All cards” section, and uses the app’s normal tower and colors.

  1. Pick stable IDs. Use lowercase names that can stay the same if you edit the wording later.
  2. Add the prompts. Each item under cards is one review card. Add a back when the card has an answer to reveal.
  3. Save the YAML file. That is all a text-only course needs. For local media, put the YAML file and assets in a .keep ZIP.
  4. Preview the import. Errors stop the import. Warnings show defaults and possible content problems.

What is required

A valid course needs only:

  • a course has schemaVersion: 2, a stable courseId, and at least one card;
  • a card has a stable cardId and something renderable on its front—text, front-side media, or both;
  • everything else can be omitted.

Leave optional fields out when you do not use them. You do not need empty arrays, blank theme values, placeholder sections, or back: "". An empty back is accepted, but the importer warns about it and treats the card as front-only.

Missing optional fields get defaults where appropriate. Invalid values still produce errors. For example, keep club can make a title when title is missing, but a title with the wrong data type is an error.

Cards

One record is one scheduled review card. Format 2 starts with one primitive: front → optional back. It does not generate multiple review directions from a note.

Front and back

schemaVersion: 2
courseId: knot-basics
title: Knot basics

cards:
  - cardId: bowline-purpose
    front: What is a bowline used for?
    back: A **fixed loop** that neither slips nor jams.

  - cardId: name-three
    front: Name three knots you can tie without looking.

The first card reveals an answer. The second is front-only, so it goes directly to self-grading instead of showing an empty answer.

A course with optional organization

schemaVersion: 2
courseId: garden-basics
title: Garden basics

sections:
  - sectionId: soil
    title: Soil
  - sectionId: watering
    title: Watering

cards:
  - cardId: compost-purpose
    sectionId: soil
    front: What does compost add to soil?
    back: Organic matter and slowly released nutrients.
    tags: [soil-health]

  - cardId: water-morning
    sectionId: watering
    front: Why water early in the morning?
    back: Less evaporates, and leaves can dry during the day.

Sections control navigation. Tags are independent labels. Neither is required for a valid course.

Markdown inside readable fields

front, back, descriptions, captions, and transcripts use the same documented CommonMark subset.

ConstructFormat 2Example
Paragraphs and line breaksYesblank line / two trailing spaces
Emphasis and strongYes*term*, **answer**
Ordered and unordered listsYes- item, 1. item
LinksHTTPS or mailto[source](https://…)
Raw HTMLShown as textnever trusted as markup
Markdown imagesNouse an explicit media object
Headings, code, quotes, rulesNodiagnosed with an inert preview
TablesLiteral textpipe syntax does not become a table

keep club validates Markdown at import and renders it safely each time a card is shown; the plain CommonMark is what it keeps. A link that is not HTTPS or mailto is an error and blocks the import. Inline HTML appears as text.

Sections, groups, and tags

You can leave out sections, groups, and tags. If you use them:

  • No sections: keep club derives one all-cards section.
  • One section: cards may omit sectionId; it is unambiguous.
  • Several sections: every card names its section.
  • Groups: if present, they place every declared section into exactly one group.
  • Tags: independent of navigation and limited to 64 per card.

Media and the .keep package

Images, audio, and video are explicit card media. Their paths point to files in the package, never remote URLs.

cards:
  - cardId: robin-song
    front: Which bird is singing?
    back: A European robin.
    media:
      - mediaId: robin-audio
        side: front
        mediaType: audio
        source: media/robin-song.opus
        mimeType: audio/ogg
        transcript: A bright, liquid series of phrases.
        credit:
          name: Example Naturalist

      - mediaId: robin-photo
        side: back
        mediaType: image
        source: media/robin.webp
        alternativeText: A robin perched on a bare branch.
        width: 1600
        height: 1067

Package those files without adding a wrapper directory:

bird-calls.keep
├── course.keep.yml
└── media
    ├── robin-song.opus
    └── robin.webp
  • course.keep.yml is at the ZIP root.
  • Paths are normalized, relative, case-stable, and cannot contain .., backslashes, URL schemes, or empty components.
  • keep club sniffs file bytes and checks them against the declared type, MIME type, and extension.
  • That check identifies the container, not every codec or damaged payload. Decode failures show as unavailable media; publication tooling should probe or transcode for its target browsers.
  • Published images need dimensions and meaningful alternative text unless decorative. Published video needs captions or a transcript.
  • Imports are atomic: a missing or broken asset cannot silently discard a card.

How validation works

  1. File checks. The importer checks the YAML structure, field names, data types, and size limits. Duplicate keys, aliases, custom tags, and multiple documents are errors.
  2. Course checks. It checks IDs and references, makes sure card fronts can render, and verifies media paths and files.
  3. Quality checks. It warns about things such as very long cards, missing attribution, weak alternative text, and cards that look like duplicates.

Errors block the whole import. Nothing is saved from a course with errors; it either all lands or all fails. Warnings may proceed after preview, and the import summary lists what landed along with every warning worth checking. Cards and assets are never silently dropped.

Every diagnostic has a stable code, severity, data path, message, correction, and documentation link. See the diagnostic code reference.

Field reference

Course

FieldRequiredMeaning when present
schemaVersionYesInteger 2.
courseIdYesStable creator-owned identity.
cardsYesOne to 50,000 card records, in presentation order.
title, shortTitle, tagline, descriptionNoHuman-facing course copy.
contentLanguage, instructionLanguageNoBCP 47-style language tags, never guessed from the device. instructionLanguage defaults to contentLanguage when omitted.
authors, license, sourceNoAttribution and provenance. A license is required to publish through keep club.
sections, groupsNoOptional navigation hierarchy.
themeNoSafe colors, packaged raster art, loading copy, and an app-owned animation.
extensionsNoNamespaced inert tool data.

Section and group

RecordRequired fieldsOptional fields and rules
SectionsectionId, titledescription, extensions. Omit the whole list for one generated all-cards section.
GroupgroupId, title, sectionIdsdescription, extensions. If groups exist, every declared section appears exactly once.

Card

FieldRequiredMeaning when present
cardIdYesStable progress identity inside the course.
frontConditionalMarkdown prompt; front media may supply the renderable front instead.
backNoMarkdown answer. Omission means front-only unless back media exists.
sectionIdNoRequired only when several declared sections make omission ambiguous.
tagsNoIndependent descriptive labels.
mediaNoUp to 16 explicit front/back media objects.
extensionsNoNamespaced inert tool data.

Media

FieldRequiredMeaning
side, mediaType, sourceYesFront/back, image/audio/video, and local package path.
mediaId, mimeTypeNoStable media identity and an assertion checked against bytes.
alternativeText, decorativeNoImage-only accessibility fields.
width, heightNoPixel dimensions for image or video media.
caption, transcript, captionTracksNoReadable and timed text alternatives.
posterImage, durationSeconds, creditNoPlayback presentation and attribution.

Attribution and provenance

RecordFieldsRule
Authorname, website, emailname is required for each author; websites use HTTPS.
Licenseidentifier, name, website, attributionOptional for private import; at least one field and a license are required for keep club publication.
Sourcewebsite, repositoryAt least one HTTPS source link when the record is present.
Media creditname, websitename is required when a credit is present.

Theme

FieldsMeaning
accentColor, accentColorDark, accentInkColor, accentInkColorDarkSix- or eight-digit hex accent and on-accent colors for light and dark presentation.
paperColor, paperColorDarkLight and dark paper colors.
shelfArtwork, sectionArtwork, loadingArtworkNormalized relative paths to packaged raster assets.
loadingTextShort loading copy; defaults to “Loading…”.
loadingAnimationnone, gentle-bob, or pulse; arbitrary CSS is never accepted.

Namespaced extensions

extensions is optional on courses, sections, groups, cards, and media. Keys use a reverse-domain namespace such as org.example.generator/template. Values stay inert: keep club preserves them but does not execute or interpret them.

The downloadable schema is the exhaustive machine-readable source: course-v2.schema.json.

Keep IDs stable

courseId identifies the course. cardId identifies one review history within it. Treat both as permanent opaque keys:

  • do not derive them from a title or the current wording;
  • do not renumber them when cards are reordered;
  • keep a card’s ID when correcting that same card;
  • use a new ID when replacing it with a genuinely different review prompt; never recycle an old ID for another fact.

keep club converts this public format to its internal data. Existing courses and card IDs still work, as do the old munin/… storage keys, cache names, and installed app identity. Those names are kept for compatibility.

Use any editor or generator

Write YAML by hand, export it from a spreadsheet, generate it from a database, or build your own course editor. The resulting file just needs to follow this format.

  • Canonical authoring: one UTF-8 YAML 1.2 document.
  • JSON is accepted as YAML-compatible plain data, but YAML is the documented human-authored form.
  • Strict known fields catch misspellings instead of discarding them.
  • Custom tool metadata belongs under a reverse-domain extensions key such as org.example.generator.
  • Scheduling, streaks, due dates, and learner history never belong in a course artifact.

keep club writes this format as well as reading it. Settings → Keeping your progress → Deck file exports the open deck as a .keep.yml, checked against this contract before the download starts. What comes out is a course file and nothing more: no review history, no notes, and no record of hidden cards. A deck stored as sanitized HTML — a built-in course or an Anki import — exports only the cards the reader wrote or changed, and a deck with packaged media exports the same, because a .keep.yml is text and its assets would not travel with it. An exported file carries extensions: app.keepclub/export naming which of the two it is and the course it came from.

Versioning

schemaVersion is a whole-number major version. The version 2 schema URL will not change. Clarifications may be added when they do not change how an accepted file is read. New fields or changed meanings require a new major version.

Tools may put their own data under namespaced extensions. keep club stores that data but does not run or interpret it.