My learning path
The Learning app does not just list courses. It leads a signed-in learner into a complete
learning path: a few questions about the project they want to build, then the courses, then a
schedule. The route has eight steps, LearningSchedule.TotalSteps:
| Steps | Stage (LearningSchedule.Stage) |
Where it happens |
|---|---|---|
| 1–6 | Questions: the LearningRoadmap questions (project, outcome, experience, confidence, pace, language) |
the learner's plan, {viewer}/LearningRoadmap/Plan |
| 7 | PickCourses: the questions are done but the route holds no course |
the plan page lists every course on the instance with Add to my path |
| 8 | Schedule: courses are planned, and some lack a start or finish day |
each stop on the plan page carries its date inputs |
| ✓ | Complete: every planned course is dated or kept for later |
the front door names what is due next |
NoPlan is the state before step 1.
The front door (Edu/Courses)
The hero card (MyLearning.Project) always shows one button that resumes exactly where the
learner stands, the step ("Step 7 of 8") and the platform's progress control:
- No plan: Build my learning path →, which opens the LearningRoadmap app (Get/Start runs in the Store, which creates the plan).
- Questions open: Continue my questions →, at the open question.
- No matching course: " — no course matched it yet. Pick yours from the full list." and Pick my courses →. The questions never end in a dead end.
- Partly scheduled: "2 courses not yet scheduled." and Schedule them →.
- Complete: the next due item, e.g. "Next: Data Modelling · Cubes by 12 March 2027", or "Overdue: …" when a day has passed, plus Open my roadmap →.
A saved legacy plan (from before the guided roadmap) just opens. The roadmap page itself offers to rebuild it.
Every course card carries the path action (MyLearning.PathActions):
- Add to my path + for a course not on the route. It opens the plan with
?add={course}, where a confirmation (RoadmapAdd) adds it with one click, says "✓ … is on your path" once it is there, or says plainly that the course is not installed on this instance and links back to the catalog. - Schedule → for a course already on the route.
- Add to my path + also appears when there is no plan yet; it starts the app.
- Nothing is shown for an anonymous visitor, or where neither the app nor a plan exists.
- The action appears only on cards the roadmap can actually add: Education plugin roots
(
LearningJourney.IsCourse), the same set its add confirmation and pick list resolve. A legacyEdu/Courseentry gets no action rather than an Add that ends in "not available". - While the questions are open (also after Refine my roadmap, which resets the step but keeps
RoadmapCreated), the add confirmation waits. The course stays named in?add=…, and the same panel becomes the Add offer once the last question is answered. The open questionnaire also wins on the front door (LearningSchedule.QuestionsOpen, the page's own rule).
The start page — a card only while it is timely
The signed-in start page (/, the person's own home) shows ONE learning card while it has something to
say. The Learning app contributes it as data: Edu/HomeCards/LearningPath is a UiContribution with
context: Home that embeds the LearningPathNudge area of Edu/Courses through the home's
@@("area/HomeCards") region (a core addition: the Home contribution context). The card is the front
door's hero, the same line, step and one resume button (MyLearning.Nudge), shown only when:
- the viewer is signed in, and
- at least one course is installed on the instance (the course roots the roadmap plans from,
LearningJourney.IsCourse). With none installed there is nothing to build a path from, so nothing is said. And - the path is not concluded: no plan yet (with the LearningRoadmap app present), questions open, nothing picked, or courses not yet scheduled.
A concluded path, a saved legacy plan (its own page offers the rebuild) and an anonymous visitor see nothing. The start page motivates; the "Next: …" line stays on the Learning front door. A failed read keeps the card hidden rather than nudging off a guess.
The plan page
The roadmap page's questionnaire progress now counts against the whole route ("Step 2 of 8").
After the questions, a guide panel under the hero shows the step, the progress and the one line
of what to do next (GuideStep / GuideLine). Two computed slots join the stops:
RoadmapPicklists the catalog's courses not on the route, smallest first (S, M, L), each with Add to my path + and Open course →. With an empty route it is titled Pick from all courses and is step 7.- Each stop's schedule (
ScheduleBlock): Start on and Finish by, and one Done by per module. A stop planned before modules were stored offers Plan it module by module, which fills them from the course's ownlearning.milestones.
They are computed slots for the same reason the stops already were: each button or input addresses its own course key, which a bound row cannot carry.
Questions the courses declare
A course declares its own roadmap questions on its root, beside its modules: declared, not derived.
"learning": {
"milestones": [ … ],
"questions": [
{ "id": "data-source", "text": "Where does your data live today?", "kind": "choice",
"topics": ["data-modeling"],
"options": [
{ "value": "excel", "label": "In spreadsheets", "topics": ["data-import"] },
{ "value": "db", "label": "In a database", "topics": ["data-modeling"] } ] } ]
}
- The roadmap asks the six base questions and then the union of the INSTALLED course roots'
questions, de-duplicated by
id(the first declaring course in path order wins). Courses that ask the same thing share an id on purpose. - A course localizes by being its own language space (
AgenticPrimer+AgenticPrimerDe): the German twin declares its questions in German, the union reads whichever editions are installed, and a shared id is asked once. There is no per-field translation. - Only
kind: "choice"is asked today (an id, a text, at least one option). Any other entry is skipped, never guessed. - The questions appear on the plan page right after the base questions (
RoadmapCourseQuestions), each a row of choices written to the plan node'scourseAnswers/{id}. They are part of the pick step, so the route stays eight steps, and they are optional. - Update my route rebuilds the rule-based route with the chosen options'
topicscounted as project topics (LearningJourney.AnswerTopics). The learner's picks and dates are kept. - Compile my path with the assistant hands everything to the Learning app's own agent (next section).
The assistant that compiles the path
Edu/Agent/LearningPathCompiler (a package agent with a distinct id, not part of the built-in roster)
receives one message built by LearningPathCompiler.Prompt: the plan node, today as a calendar day,
the base answers, each course question with its answer, the installed courses only (size, language,
topics, modules m00, m01, …), and the courses already on the route, marking the ones the learner
picked. It chooses and orders the courses, proposes start, finish and module days, and writes them to the
plan's content.courses. It keeps every picked course and every note, and moves a dropped recommendation
to previousPlan rather than deleting it. The thread runs in the learner's plan, with the plan as its
context, and opens for them to follow. The rule-based route stays the default, so a path never depends on
a model being configured.
The data — the learner's own typed node
Everything is stored on the learner's own plan node (Edu/LearningJourney,
LearningJourneyContent). Nothing is copied into a layout-area replica:
LearningCourse.TargetStart,TargetFinish(DateTime?);LearningCourse.Milestones:m00,m01, … →LearningMilestone { Order, Title, Path, Due }, filled from the course'slearning.milestoneswhen the course is recommended or added.
Every date input is a DateTimeControl bound directly to the plan node at
courses/{key}/targetStart, …/targetFinish and …/milestones/{m}/due, the same way the stop's
notes already were. There is no save button and no /data copy.
🚨 Dates are calendar facts. A target is the day the learner picked. It is shown as that day
(LearningSchedule.DateText), compared as a day, and never converted to a time zone; a test pins
that 23:30 UTC on the 12th still reads "12 March". "Today" for the overdue check is UTC's date: a
target is a day, and a boundary a few hours off for one viewer only changes whether that day already
counts as overdue.
A course the learner adds is marked Picked, and a rebuilt recommendation keeps picked courses
on the route; only recommended courses outside the new route move to an earlier plan. Completion
for the next due item reads the plan's own courses as well as the catalog's, so a finished course
the catalog no longer lists is never advertised as due.
Adding never duplicates (LearningJourney.AddFromCatalog). A course already on the route is left
exactly as it is, and an entry kept from an earlier plan comes back with its own dates. Rebuilding
the route after new answers keeps the learner's schedule: the course dates, and each module's date
matched by its page wherever it now sits.
LearningCourse has value equality including the module map. A record compares an
ImmutableDictionary by reference, which made two identical recommendations read as different.
Tests
Edu/LearningJourney → CourseQuestionsTests (the union, de-duplication and twins, answers feeding the route, the assistant's message, the panel); Edu/CourseCatalog → MyLearningTests.Nudge_OnlyWhileTimely (no course installed, anonymous, no app, each open stage, concluded, legacy, German).
Edu/LearningJourney → LearningScheduleTests (every route state, next due item, calendar dates,
adding, rebuild, the guide, the add confirmation) and Edu/CourseCatalog →
MyLearningTests.Hero_LeadsThroughEveryState (no plan, questions, no match, partial, complete,
German) and Cards_OfferAddOrSchedule.