Skip to content

The lesson model

Everything you author in Itera hangs off a four-level graph. Understand these four rows and their shapes and the 21 tools become obvious — each is just create/read/update/delete on one level.

flowchart TD
  tenant["Tenant<br/><small>your org / batch</small>"]
  program["Program<br/><small>a course template · unique slug per tenant</small>"]
  unit["Unit<br/><small>a section · ordinal</small>"]
  lesson["Lesson<br/><small>a single class · ordinal</small>"]
  activity["Activity<br/><small>kind · spec · rubric · ordinal</small>"]
  tenant --> program --> unit --> lesson --> activity
LevelLives inKey fieldsParent
Programitera.programsslug (unique per tenant), title_i18ntenant_id
Unititera.unitsordinal, title_i18nprogram_id
Lessonitera.lessonsordinal, title_i18nunit_id
Activityitera.activitieskind, spec (jsonb), rubric (jsonb), ordinallesson_id
  • Each level is created with the parent id returned by the previous create_* call.
  • ordinal is the sort position within its parent (default 0).
  • Deleting a parent cascades to its children (deleting a program removes its units, lessons, and activities).
  • Everything is tenant-scoped by RLS. You only see and touch tenants your identity belongs to.

Titles are an i18n map (jsonb), e.g. { "en": "Intro", "pt": "Introdução" }.

  • You may pass a bare string — it is coerced to { "en": "<string>" }.
  • Omitting it defaults to {}.
// both are valid title_i18n inputs
"Introduction to Kafka"
{ "en": "Introduction to Kafka", "pt": "Introdução ao Kafka" }

activity.kind is one of ten values. The first four are the original dev/coding kinds; the rest are the general learning-activity taxonomy.

KindGroupWhat it is
diagramjudgedBuild/annotate a diagram; graded against spec + rubric.
documentjudgedWrite a document/answer; AI-judged against the rubric.
quizjudgedA quiz (question set) evaluated against the rubric.
codejudgedA coding task; the artifact is judged.
conceptno-judgeA concept / reading / explainer screen (no grading).
single_choiceno-judgePick exactly one option.
multi_choiceno-judgePick one or more options.
matchno-judgeMatch pairs across two columns.
orderno-judgePut items in the correct order.
videono-judgeWatch a video (optionally gated on completion).

Both are jsonb and both default to {}. They are kind-specific:

  • spec — the activity’s configuration/content. For a single_choice, the prompt + options; for a document, the task statement; for a video, the video reference; etc.
  • rubric — the grading criteria (for judged kinds). For a document or code activity, the rubric drives the AI judge’s verdict.

The authoring tools do not enforce a per-kind spec schema — they store what you give them. Keep the shape consistent per kind so the player and the judge can read it. Illustrative shapes:

single_choice · spec
{
"prompt": "Which delivery guarantee does a Kafka consumer group provide by default?",
"options": [
{ "id": "a", "label": "Exactly-once" },
{ "id": "b", "label": "At-least-once" },
{ "id": "c", "label": "At-most-once" }
],
"answer": "b"
}
document · spec + rubric
// spec
{ "prompt": "Explain how consumer-group rebalancing works and one way to minimise its impact." }
// rubric
{
"criteria": [
{ "criterion": "Describes partition reassignment on membership change", "weight": 0.5 },
{ "criterion": "Names a mitigation (e.g. static membership / cooperative rebalance)", "weight": 0.5 }
],
"pass_score": 70
}

Two reads return the graph pre-joined so you don’t have to walk it level by level:

  • get_program returns a program with its full nested graph (units → lessons → activities, ordered by ordinal).
  • instantiate_program deep-copies that whole graph into a new program.