Course → Unit → Lesson → Activity → Question, with Cohort as the run-time instance and free
metadata + external_ids on every resource. That tiny set covers
everything from a flat Google-Classroom-style course to a deeply nested
regulated training program — without forcing a domain-specific schema
on you.
This page shows three concrete examples that look very different on
paper but reuse the same primitives.
The mental model in one paragraph
A Course is the curriculum. A Cohort is one run of it. A Unit groups lessons and can nest into other units viaparent_unit_id when you need extra depth. A Lesson is the atomic
piece of content (markdown, video, reading). An Activity is
something the student does (quiz, flashcard, assignment). Anything
domain-specific (SEPE codes, CRNs, FUNDAE file numbers, Canvas IDs)
lives in metadata and external_ids — never in dedicated columns.
Example 1 — Replicate a Moodle course
Moodle’s structure is shallow:course → section → activity/resource.
Map it like this:
external_ids, you
can re-export the course back into the same Moodle instance and the
mapping survives.
Example 2 — Spanish vocational training (especialidad formativa)
A SEPE-registered especialidad formativa has three internal levels: módulos formativos → unidades formativas → epígrafes. This is where nestable units pay rent — you setparent_unit_id to compose
depth without inventing new resources.
Section resource, no
Epigrafe resource, no EspecialidadFormativa subclass. Depth comes
from parent_unit_id; domain semantics come from metadata. The same
SDK call shape works.
If you ship many courses in Spain and want typed fields (
codigoSepe
instead of metadata.codigo_sepe), the recommended path is a
companion package (e.g. @hawkings/sdk-spain) that wraps these
calls, not core SDK changes. See the Companion packages
guide for the pattern.Example 3 — A US university subject
A typical US college course (say CS 101 — Introduction to Computer Science) lives once in the catalog but is delivered multiple times per academic year, each section with its own instructor, schedule, and enrollment. This is the textbook use case for the Course / Cohort split.Course and decide
explicitly whether existing cohorts inherit or stay frozen.
Example 4 — A program bundling multiple courses
Catalog model: you have a library of ~100 standalone courses (Python, Anthropic API, RAG, prompt engineering…). You don’t only sell them individually — you also sell named bundles like “AI Specialization” which packages three of them together, with a defined start and end date per intake. This is the same pattern as Coursera Specializations, edX MicroMasters, and Udacity Nanodegrees. It’s modeled with a third top-level resource:Program.
The three resources in play
The key design choice:
Program is lightweight. It does not
duplicate content; it references courses. If you update Python in
the catalog, every program that includes it gets the update for free.
The non-obvious question: what happens when a student enrolls into a program cohort?
You have to make a product decision. Both options are valid and the SDK supports both — pick the simpler one unless you have a reason not to.Option A — Global access, unified progress (recommended default)
When Alice is enrolled inintake2026:
- She automatically has access to the full content tree of all three referenced courses.
- Her progress, submissions, grades and certificates live under the program cohort, not in separate per-course cohorts.
- No sub-cohorts are created. The program cohort is the only container.
cohort.metadata.schedule or a small cohortSchedule resource.
Option B — Auto-generated sub-cohorts
Creating the program cohort transparently spins up one cohort per referenced course, all linked back:Content that belongs to the program, not to any single course
What if AI Specialization has its own onboarding video, capstone project, or graduation exam — content that doesn’t naturally belong to Python, Anthropic, or RAG individually? The clean answer: create it as a course, then add it to the program’scourse_ids:
access_via: "program_only" metadata lets your application hide
this course from the public catalog — the model itself doesn’t need a
special “program-exclusive” type.
Quick reference
This is structurally the same shape Stripe uses:
Subscription covers
many SubscriptionItems, each pointing at a Price for a Product.
Here: a Program Cohort covers many referenced Courses. Same
pattern, different domain.
The pattern, in one screen
Across all three examples — wildly different domains, depth, and vocabulary — you used the same five primitives:1
Course
The curriculum. Same shape whether it’s a 4-week Moodle course or
an 880-hour SEPE program.
2
Unit (nestable)
Top-level when you need flat structure,
parent_unit_id when you
need depth. No Section, no Epigraph, no domain subclasses.3
Lesson + Activity + Question
The leaves. Content, exercises, questions — typed by a
type
discriminator, not by separate resources.4
Cohort
The runtime instance. Carries dates, students, teachers, and any
per-run overrides.
5
metadata + external_ids
Where domain-specific data lives. SEPE codes, CRNs, Canvas IDs,
FUNDAE expedients — everything that’s specific to your market or
LMS without polluting the core schema.
When you actually need more
The model is deliberately small. Reach for one of these only when the core primitives genuinely can’t express what you need:Where Program fits — and where it does not
Program exists in the SDK but it is not a mandatory hierarchy
level above Course. It is a lateral resource you reach for only
when you need it. Two patterns to keep straight:
- Grouping multiple courses into a sellable bundle (a master’s, a
specialization, a learning path) → use
programsas shown in Example 4. Lightweight, many-to-many with courses, with its own cohorts and prices. - Domain hierarchy inside a single course (e.g. a SEPE
especialidad containing módulos formativos → unidades →
epígrafes) → that is not a program. It is a single
Coursewith nestedUnits, as shown in Example 2.
Program — just use the Course directly. Like Stripe doesn’t force
you to wrap every Product in a ProductCatalog, Hawkings doesn’t
force you to wrap every Course in a Program. Reach for it when
you’re actually bundling.