Start small
The same format works for a two-card course and a large course. Add optional fields as you need them.
Course format 2
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.
The same format works for a two-card course and a large course. Add optional fields as you need them.
The public fields have names such as courseId,
cardId, front, and back.
Review history, due dates, and other learner data are stored by the app, not in the course file.
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.
cards is one review card. Add a back when
the card has an answer to reveal..keep ZIP.A valid course needs only:
schemaVersion: 2, a stable
courseId, and at least one card;cardId and something renderable
on its front—text, front-side media, or both;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.
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.
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.
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.
front, back, descriptions, captions, and
transcripts use the same documented CommonMark subset.
| Construct | Format 2 | Example |
|---|---|---|
| Paragraphs and line breaks | Yes | blank line / two trailing spaces |
| Emphasis and strong | Yes | *term*, **answer** |
| Ordered and unordered lists | Yes | - item, 1. item |
| Links | HTTPS or mailto | [source](https://…) |
| Raw HTML | Shown as text | never trusted as markup |
| Markdown images | No | use an explicit media object |
| Headings, code, quotes, rules | No | diagnosed with an inert preview |
| Tables | Literal text | pipe 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.
You can leave out sections, groups, and tags. If you use them:
all-cards section.sectionId; it is unambiguous..keep packageImages, 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..., backslashes, URL schemes, or empty components.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 | Required | Meaning when present |
|---|---|---|
schemaVersion | Yes | Integer 2. |
courseId | Yes | Stable creator-owned identity. |
cards | Yes | One to 50,000 card records, in presentation order. |
title, shortTitle, tagline, description | No | Human-facing course copy. |
contentLanguage, instructionLanguage | No | BCP 47-style language tags, never guessed from the device. instructionLanguage defaults to contentLanguage when omitted. |
authors, license, source | No | Attribution and provenance. A license is required to publish through keep club. |
sections, groups | No | Optional navigation hierarchy. |
theme | No | Safe colors, packaged raster art, loading copy, and an app-owned animation. |
extensions | No | Namespaced inert tool data. |
| Record | Required fields | Optional fields and rules |
|---|---|---|
| Section | sectionId, title | description, extensions. Omit the whole list for one generated all-cards section. |
| Group | groupId, title, sectionIds | description, extensions. If groups exist, every declared section appears exactly once. |
| Field | Required | Meaning when present |
|---|---|---|
cardId | Yes | Stable progress identity inside the course. |
front | Conditional | Markdown prompt; front media may supply the renderable front instead. |
back | No | Markdown answer. Omission means front-only unless back media exists. |
sectionId | No | Required only when several declared sections make omission ambiguous. |
tags | No | Independent descriptive labels. |
media | No | Up to 16 explicit front/back media objects. |
extensions | No | Namespaced inert tool data. |
| Field | Required | Meaning |
|---|---|---|
side, mediaType, source | Yes | Front/back, image/audio/video, and local package path. |
mediaId, mimeType | No | Stable media identity and an assertion checked against bytes. |
alternativeText, decorative | No | Image-only accessibility fields. |
width, height | No | Pixel dimensions for image or video media. |
caption, transcript, captionTracks | No | Readable and timed text alternatives. |
posterImage, durationSeconds, credit | No | Playback presentation and attribution. |
| Record | Fields | Rule |
|---|---|---|
| Author | name, website, email | name is required for each author; websites use HTTPS. |
| License | identifier, name, website, attribution | Optional for private import; at least one field and a license are required for keep club publication. |
| Source | website, repository | At least one HTTPS source link when the record is present. |
| Media credit | name, website | name is required when a credit is present. |
| Fields | Meaning |
|---|---|
accentColor, accentColorDark, accentInkColor, accentInkColorDark | Six- or eight-digit hex accent and on-accent colors for light and dark presentation. |
paperColor, paperColorDark | Light and dark paper colors. |
shelfArtwork, sectionArtwork, loadingArtwork | Normalized relative paths to packaged raster assets. |
loadingText | Short loading copy; defaults to “Loading…”. |
loadingAnimation | none, gentle-bob, or pulse; arbitrary CSS is never accepted. |
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.
courseId identifies the course.
cardId identifies one review history within it. Treat
both as permanent opaque keys:
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.
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.
extensions key such as
org.example.generator.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.
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.