> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hawkings.education/llms.txt
> Use this file to discover all available pages before exploring further.

# Modeling real-world courses

> Three worked examples — a Moodle course, a Spanish vocational training program, and a US university subject — using the same Hawkings primitives.

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:

| Moodle                      | Hawkings                       |
| --------------------------- | ------------------------------ |
| `course`                    | `Course`                       |
| `section` (week or topic)   | `Unit` (top-level)             |
| `mod_page` / `mod_resource` | `Lesson` with `type: markdown` |
| `mod_quiz`                  | `Activity` with `type: quiz`   |
| `mod_assign`                | `Assignment`                   |

<CodeGroup>
  ```ts TypeScript theme={null}
  const course = await hk.courses.create({
    title: "Introduction to Python",
    description: "An 8-week intro to programming",
    metadata: {
      moodle_format: "weeks",
      moodle_category: "Programming"
    },
    external_ids: { moodle: "course_1234" }
  });

  const week1 = await hk.units.create({
    course_id: course.id,
    title: "Week 1 — Syntax basics",
    position: 1,
    external_ids: { moodle: "section_1" }
  });

  await hk.lessons.create({
    unit_id: week1.id,
    type: "markdown",
    title: "Variables and types",
    content: "# Variables\n\nIn Python...",
    external_ids: { moodle: "course_module_45" }
  });

  const quiz = await hk.activities.create({
    unit_id: week1.id,
    type: "quiz",
    title: "End-of-week 1 quiz",
    external_ids: { moodle: "course_module_46" }
  });

  await hk.questions.create({
    activity_id: quiz.id,
    type: "multiple_choice",
    prompt: "What is the type of 3.14?",
    options: ["int", "float", "str"],
    correct_answer: 1
  });
  ```

  ```php PHP theme={null}
  $course = $hk->courses->create([
      'title' => 'Introduction to Python',
      'description' => 'An 8-week intro to programming',
      'metadata' => [
          'moodle_format' => 'weeks',
          'moodle_category' => 'Programming',
      ],
      'external_ids' => ['moodle' => 'course_1234'],
  ]);

  $week1 = $hk->units->create([
      'course_id' => $course->id,
      'title' => 'Week 1 — Syntax basics',
      'position' => 1,
      'external_ids' => ['moodle' => 'section_1'],
  ]);

  $hk->lessons->create([
      'unit_id' => $week1->id,
      'type' => 'markdown',
      'title' => 'Variables and types',
      'content' => "# Variables\n\nIn Python...",
      'external_ids' => ['moodle' => 'course_module_45'],
  ]);

  $quiz = $hk->activities->create([
      'unit_id' => $week1->id,
      'type' => 'quiz',
      'title' => 'End-of-week 1 quiz',
      'external_ids' => ['moodle' => 'course_module_46'],
  ]);

  $hk->questions->create([
      'activity_id' => $quiz->id,
      'type' => 'multiple_choice',
      'prompt' => 'What is the type of 3.14?',
      'options' => ['int', 'float', 'str'],
      'correct_answer' => 1,
  ]);
  ```
</CodeGroup>

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.

| Especialidad formativa       | Hawkings                          |
| ---------------------------- | --------------------------------- |
| Especialidad (e.g. ADGD0308) | `Course`                          |
| Módulo formativo (MF)        | `Unit` (top-level)                |
| Unidad formativa (UF)        | `Unit` with `parent_unit_id = MF` |
| Epígrafe                     | `Lesson`                          |
| Convocatoria                 | `Cohort`                          |

<CodeGroup>
  ```ts TypeScript theme={null}
  const af = await hk.courses.create({
    title: "Actividades de gestión administrativa",
    metadata: {
      codigo_sepe: "ADGD0308",
      familia_profesional: "Administración y gestión",
      nivel_qualificacion: "2",
      horas_totales: "880"
    }
  });

  // Módulo formativo (top-level unit)
  const mf1 = await hk.units.create({
    course_id: af.id,
    title: "MF0233_2 — Ofimática",
    position: 1,
    metadata: {
      codigo_mf: "MF0233_2",
      horas: "190",
      transversal: true
    }
  });

  // Unidad formativa nested inside MF1
  const uf1 = await hk.units.create({
    course_id: af.id,
    parent_unit_id: mf1.id,
    title: "UF0319 — Sistema operativo y búsqueda de información",
    position: 1,
    metadata: { codigo_uf: "UF0319", horas: "30" }
  });

  // Epígrafes = lessons
  await hk.lessons.create({
    unit_id: uf1.id,
    type: "markdown",
    title: "1.1 Introducción al ordenador",
    content: "...",
    metadata: { epigrafe: "1.1" }
  });

  await hk.lessons.create({
    unit_id: uf1.id,
    type: "markdown",
    title: "1.2 Hardware básico",
    content: "...",
    metadata: { epigrafe: "1.2" }
  });

  // The May 2026 cohort being delivered to a specific group
  const convocatoria = await hk.cohorts.create({
    course_id: af.id,
    title: "Convocatoria mayo 2026 — Madrid",
    starts_at: "2026-05-20",
    ends_at: "2026-11-20",
    metadata: {
      expediente_fundae: "F262345",
      modalidad: "teleformacion",
      centro: "Centro Madrid"
    }
  });

  await hk.enrollments.create({
    cohort_id: convocatoria.id,
    student_id: "usr_alice",
    role: "student"
  });
  ```

  ```php PHP theme={null}
  $af = $hk->courses->create([
      'title' => 'Actividades de gestión administrativa',
      'metadata' => [
          'codigo_sepe' => 'ADGD0308',
          'familia_profesional' => 'Administración y gestión',
          'nivel_qualificacion' => '2',
          'horas_totales' => '880',
      ],
  ]);

  // Módulo formativo (top-level unit)
  $mf1 = $hk->units->create([
      'course_id' => $af->id,
      'title' => 'MF0233_2 — Ofimática',
      'position' => 1,
      'metadata' => [
          'codigo_mf' => 'MF0233_2',
          'horas' => '190',
          'transversal' => true,
      ],
  ]);

  // Unidad formativa nested inside MF1
  $uf1 = $hk->units->create([
      'course_id' => $af->id,
      'parent_unit_id' => $mf1->id,
      'title' => 'UF0319 — Sistema operativo y búsqueda de información',
      'position' => 1,
      'metadata' => ['codigo_uf' => 'UF0319', 'horas' => '30'],
  ]);

  // Epígrafes = lessons
  $hk->lessons->create([
      'unit_id' => $uf1->id,
      'type' => 'markdown',
      'title' => '1.1 Introducción al ordenador',
      'content' => '...',
      'metadata' => ['epigrafe' => '1.1'],
  ]);

  $hk->lessons->create([
      'unit_id' => $uf1->id,
      'type' => 'markdown',
      'title' => '1.2 Hardware básico',
      'content' => '...',
      'metadata' => ['epigrafe' => '1.2'],
  ]);

  // The May 2026 cohort being delivered to a specific group
  $convocatoria = $hk->cohorts->create([
      'course_id' => $af->id,
      'title' => 'Convocatoria mayo 2026 — Madrid',
      'starts_at' => '2026-05-20',
      'ends_at' => '2026-11-20',
      'metadata' => [
          'expediente_fundae' => 'F262345',
          'modalidad' => 'teleformacion',
          'centro' => 'Centro Madrid',
      ],
  ]);

  $hk->enrollments->create([
      'cohort_id' => $convocatoria->id,
      'student_id' => 'usr_alice',
      'role' => 'student',
  ]);
  ```
</CodeGroup>

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.

<Info>
  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](/guides/companion-packages) for the pattern.
</Info>

***

## 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**.

| University                           | Hawkings                  |
| ------------------------------------ | ------------------------- |
| Subject / catalog entry (CS 101)     | `Course`                  |
| Course offering (Fall 2026 sec. 001) | `Cohort`                  |
| Week / unit                          | `Unit` (top-level)        |
| Topic / lecture                      | `Lesson`                  |
| Problem set / quiz / midterm         | `Activity` / `Assignment` |

<CodeGroup>
  ```ts TypeScript theme={null}
  const cs101 = await hk.courses.create({
    title: "Introduction to Computer Science",
    description: "Foundational CS using Python",
    metadata: {
      course_code: "CS 101",
      department: "Computer Science",
      credits: "4",
      catalog_level: "undergraduate-100"
    }
  });

  const week1 = await hk.units.create({
    course_id: cs101.id,
    title: "Week 1 — Computation and variables",
    position: 1,
    metadata: { week_number: "1" }
  });

  await hk.lessons.create({
    unit_id: week1.id,
    type: "video",
    title: "Lecture 1.1 — What is computation?",
    content: { video_url: "...", duration_seconds: 2700 }
  });

  await hk.lessons.create({
    unit_id: week1.id,
    type: "markdown",
    title: "Reading — Sipser Ch. 1.1",
    content: "..."
  });

  const pset1 = await hk.assignments.create({
    unit_id: week1.id,
    title: "Problem Set 1",
    points_possible: 100,
    due_offset_days: 7
  });

  // Two different runs of the same course, no content duplication
  const fall2026 = await hk.cohorts.create({
    course_id: cs101.id,
    title: "CS 101 — Fall 2026, Section 001",
    starts_at: "2026-09-08",
    ends_at: "2026-12-18",
    metadata: {
      term: "Fall 2026",
      section_number: "001",
      crn: "12345",
      meeting_pattern: "MWF 10:00-10:50",
      room: "Stata 32-123"
    }
  });

  const spring2027 = await hk.cohorts.create({
    course_id: cs101.id,
    title: "CS 101 — Spring 2027, Section 001",
    starts_at: "2027-01-15",
    ends_at: "2027-05-20",
    metadata: { term: "Spring 2027", crn: "67890" }
  });

  // Per-cohort schedule override on the shared content
  await hk.cohorts.scheduleAssignment({
    cohort_id: fall2026.id,
    assignment_id: pset1.id,
    due_date: "2026-09-22T23:59:00Z"
  });

  // LTI launch — if the university runs Canvas as the official LMS
  await hk.cohorts.connectLTI({
    cohort_id: fall2026.id,
    platform_issuer: "https://canvas.school.edu",
    context_id: "canvas_course_99887"
  });
  ```

  ```php PHP theme={null}
  $cs101 = $hk->courses->create([
      'title' => 'Introduction to Computer Science',
      'description' => 'Foundational CS using Python',
      'metadata' => [
          'course_code' => 'CS 101',
          'department' => 'Computer Science',
          'credits' => '4',
          'catalog_level' => 'undergraduate-100',
      ],
  ]);

  $week1 = $hk->units->create([
      'course_id' => $cs101->id,
      'title' => 'Week 1 — Computation and variables',
      'position' => 1,
      'metadata' => ['week_number' => '1'],
  ]);

  $hk->lessons->create([
      'unit_id' => $week1->id,
      'type' => 'video',
      'title' => 'Lecture 1.1 — What is computation?',
      'content' => ['video_url' => '...', 'duration_seconds' => 2700],
  ]);

  $hk->lessons->create([
      'unit_id' => $week1->id,
      'type' => 'markdown',
      'title' => 'Reading — Sipser Ch. 1.1',
      'content' => '...',
  ]);

  $pset1 = $hk->assignments->create([
      'unit_id' => $week1->id,
      'title' => 'Problem Set 1',
      'points_possible' => 100,
      'due_offset_days' => 7,
  ]);

  // Two different runs of the same course, no content duplication
  $fall2026 = $hk->cohorts->create([
      'course_id' => $cs101->id,
      'title' => 'CS 101 — Fall 2026, Section 001',
      'starts_at' => '2026-09-08',
      'ends_at' => '2026-12-18',
      'metadata' => [
          'term' => 'Fall 2026',
          'section_number' => '001',
          'crn' => '12345',
          'meeting_pattern' => 'MWF 10:00-10:50',
          'room' => 'Stata 32-123',
      ],
  ]);

  $spring2027 = $hk->cohorts->create([
      'course_id' => $cs101->id,
      'title' => 'CS 101 — Spring 2027, Section 001',
      'starts_at' => '2027-01-15',
      'ends_at' => '2027-05-20',
      'metadata' => ['term' => 'Spring 2027', 'crn' => '67890'],
  ]);

  // Per-cohort schedule override on the shared content
  $hk->cohorts->scheduleAssignment([
      'cohort_id' => $fall2026->id,
      'assignment_id' => $pset1->id,
      'due_date' => '2026-09-22T23:59:00Z',
  ]);

  // LTI launch — if the university runs Canvas as the official LMS
  $hk->cohorts->connectLTI([
      'cohort_id' => $fall2026->id,
      'platform_issuer' => 'https://canvas.school.edu',
      'context_id' => 'canvas_course_99887',
  ]);
  ```
</CodeGroup>

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

| Resource  | What it is                                                               | Has its own content tree?         |
| --------- | ------------------------------------------------------------------------ | --------------------------------- |
| `Course`  | Atomic catalog item (Python, RAG, etc.). Has its own units/lessons.      | **Yes**                           |
| `Program` | A named bundle that references `course_ids`. Has title, description.     | **No** (delegates to its courses) |
| `Cohort`  | One time-boxed run. Belongs to **either** a `course` **or** a `program`. | —                                 |

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.

<CodeGroup>
  ```ts TypeScript theme={null}
  // 1. The catalog of standalone courses
  const python = await hk.courses.create({
    title: "Python from zero",
    description: "Fundamentals of Python programming"
  });
  const anthropic = await hk.courses.create({
    title: "Building with the Anthropic API",
    description: "Claude, tool use, agents"
  });
  const rag = await hk.courses.create({
    title: "Production-grade RAG",
    description: "Retrieval, embeddings, evals"
  });

  // 2. The bundle: a lightweight program referencing courses
  const aiSpec = await hk.programs.create({
    title: "AI Specialization",
    description: "Master Python, LLM APIs and RAG in six months",
    course_ids: [python.id, anthropic.id, rag.id],
    metadata: {
      estimated_hours: "180",
      level: "intermediate",
      certificate: "true"
    }
  });

  // 3a. Cohort attached to the PROGRAM — the 2026 intake of the bundle
  const intake2026 = await hk.cohorts.create({
    program_id: aiSpec.id,           // ← program_id, not course_id
    title: "AI Specialization — 2026 cohort",
    starts_at: "2026-01-15",
    ends_at: "2026-07-15",
    metadata: { capacity: "100" }
  });

  // 3b. Or cohort attached to a single course — for à-la-carte buyers
  const pythonQ1 = await hk.cohorts.create({
    course_id: python.id,
    title: "Python — Q1 2026"
  });

  // 4. Pricing — Prices point at either a course or a program
  await hk.prices.create({
    program_id: aiSpec.id,
    amount: 99900,
    currency: "EUR",
    type: "one_time"
  });
  await hk.prices.create({
    course_id: python.id,
    amount: 19900,
    currency: "EUR",
    type: "one_time"
  });

  // 5. Enrollments — same primitive, different cohort
  await hk.enrollments.create({
    cohort_id: intake2026.id,        // bought the bundle
    student_id: "usr_alice"
  });
  await hk.enrollments.create({
    cohort_id: pythonQ1.id,          // bought Python only
    student_id: "usr_bob"
  });
  ```

  ```php PHP theme={null}
  // 1. The catalog of standalone courses
  $python = $hk->courses->create([
      'title' => 'Python from zero',
      'description' => 'Fundamentals of Python programming',
  ]);
  $anthropic = $hk->courses->create([
      'title' => 'Building with the Anthropic API',
      'description' => 'Claude, tool use, agents',
  ]);
  $rag = $hk->courses->create([
      'title' => 'Production-grade RAG',
      'description' => 'Retrieval, embeddings, evals',
  ]);

  // 2. The bundle: a lightweight program referencing courses
  $aiSpec = $hk->programs->create([
      'title' => 'AI Specialization',
      'description' => 'Master Python, LLM APIs and RAG in six months',
      'course_ids' => [$python->id, $anthropic->id, $rag->id],
      'metadata' => [
          'estimated_hours' => '180',
          'level' => 'intermediate',
          'certificate' => 'true',
      ],
  ]);

  // 3a. Cohort attached to the PROGRAM — the 2026 intake of the bundle
  $intake2026 = $hk->cohorts->create([
      'program_id' => $aiSpec->id,        // ← program_id, not course_id
      'title' => 'AI Specialization — 2026 cohort',
      'starts_at' => '2026-01-15',
      'ends_at' => '2026-07-15',
      'metadata' => ['capacity' => '100'],
  ]);

  // 3b. Or cohort attached to a single course — for à-la-carte buyers
  $pythonQ1 = $hk->cohorts->create([
      'course_id' => $python->id,
      'title' => 'Python — Q1 2026',
  ]);

  // 4. Pricing — Prices point at either a course or a program
  $hk->prices->create([
      'program_id' => $aiSpec->id,
      'amount' => 99900,
      'currency' => 'EUR',
      'type' => 'one_time',
  ]);
  $hk->prices->create([
      'course_id' => $python->id,
      'amount' => 19900,
      'currency' => 'EUR',
      'type' => 'one_time',
  ]);

  // 5. Enrollments — same primitive, different cohort
  $hk->enrollments->create([
      'cohort_id' => $intake2026->id,     // bought the bundle
      'student_id' => 'usr_alice',
  ]);
  $hk->enrollments->create([
      'cohort_id' => $pythonQ1->id,       // bought Python only
      'student_id' => 'usr_bob',
  ]);
  ```
</CodeGroup>

### 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 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:

```
intake2026 (program cohort)
  ├─ intake2026-python      (course cohort)
  ├─ intake2026-anthropic   (course cohort)
  └─ intake2026-rag         (course cohort)
```

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:

<CodeGroup>
  ```ts TypeScript theme={null}
  await hk.programs.create({
    // ...
    cohort_strategy: "subcohorts"   // default is "unified"
  });
  ```

  ```php PHP theme={null}
  $hk->programs->create([
      // ...
      'cohort_strategy' => 'subcohorts',   // default is "unified"
  ]);
  ```
</CodeGroup>

### 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`:

<CodeGroup>
  ```ts TypeScript theme={null}
  const capstone = await hk.courses.create({
    title: "AI Specialization — Capstone Project",
    description: "Final cross-disciplinary project",
    metadata: {
      capstone: "true",
      access_via: "program_only"     // your business layer enforces this
    }
  });

  await hk.programs.update(aiSpec.id, {
    course_ids: [python.id, anthropic.id, rag.id, capstone.id]
  });
  ```

  ```php PHP theme={null}
  $capstone = $hk->courses->create([
      'title' => 'AI Specialization — Capstone Project',
      'description' => 'Final cross-disciplinary project',
      'metadata' => [
          'capstone' => 'true',
          'access_via' => 'program_only',    // your business layer enforces this
      ],
  ]);

  $hk->programs->update($aiSpec->id, [
      'course_ids' => [$python->id, $anthropic->id, $rag->id, $capstone->id],
  ]);
  ```
</CodeGroup>

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

| Question                                 | Answer                                                            |
| ---------------------------------------- | ----------------------------------------------------------------- |
| Is `Program` a required hierarchy level? | No. Optional, lightweight, lateral.                               |
| Can a course exist with no program?      | Yes. Most courses will.                                           |
| Can a course be in multiple programs?    | Yes. Many-to-many.                                                |
| Does a cohort always have a `course_id`? | No. It has `course_id` **XOR** `program_id`.                      |
| Where does pricing go?                   | `prices` — points to either `course_id` or `program_id`.          |
| Where does the program's content live?   | In the referenced courses. The program is just the bundle.        |
| Can the program have exclusive content?  | Yes — model it as a course, mark it `access_via: "program_only"`. |

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:

<Steps>
  <Step title="Course">
    The curriculum. Same shape whether it's a 4-week Moodle course or
    an 880-hour SEPE program.
  </Step>

  <Step title="Unit (nestable)">
    Top-level when you need flat structure, `parent_unit_id` when you
    need depth. No `Section`, no `Epigraph`, no domain subclasses.
  </Step>

  <Step title="Lesson + Activity + Question">
    The leaves. Content, exercises, questions — typed by a `type`
    discriminator, not by separate resources.
  </Step>

  <Step title="Cohort">
    The runtime instance. Carries dates, students, teachers, and any
    per-run overrides.
  </Step>

  <Step title="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.
  </Step>
</Steps>

## 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:

| You want to…                                              | Use                                                                   |
| --------------------------------------------------------- | --------------------------------------------------------------------- |
| Group multiple courses (master, itinerary, learning path) | `programs` (lightweight many-to-many)                                 |
| Sell courses or cohorts                                   | `prices` + `checkoutSessions`                                         |
| Map content into a third-party LMS                        | `external_ids` + the relevant exporter (SCORM, Common Cartridge, LTI) |
| Type strongly the domain-specific fields                  | A companion package, not core SDK                                     |
| Track competencies / outcomes                             | `outcomes` (CASE-aligned, v2)                                         |

## 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 formativos* → *unidades* →
  *epí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.
