Skip to main content
The data model is intentionally minimal: 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 via parent_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:
Because the original Moodle IDs are preserved in 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 set parent_unit_id to compose depth without inventing new resources.
Note what you did not have to do: no 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.
One canonical CS 101. Two cohorts. Zero content duplication. When the syllabus changes mid-year, you update the 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. When Alice is enrolled in intake2026:
  • 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.
Pros: one source of truth for progress (“Alice has completed 73% of the program”), simple dashboards, simple billing. Cons: if you want the three courses to follow staggered schedules inside the program (Python in months 1-2, Anthropic in months 3-4, RAG in months 5-6), you’ll need to express that with 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:
Alice is enrolled in all four. Pros: each course keeps its own teacher, schedule, and rules independently. Closer to how universities model multi-course programs. Cons: ambiguity about which cohort is the source of truth for program-level progress. More moving parts. To opt in:

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’s course_ids:
The 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 programs as 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 formativosunidadesepígrafes) → that is not a program. It is a single Course with nested Units, as shown in Example 2.
If your “program” only contains a single course, you don’t need a 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.