Recurring LP / Course Reassignment

Design Proposal — LP-Sequential with Extensions

Approach: LP-Sequential with Extensions

Extend the existing Learning Program structure to support recurring certification. The admin builds an LP as a sequence of courses, each with its own activation rule and optional end date. The LP is a stable container that grows over time — each certification cycle is a Course (or set of components) added to it.

Why this approach:
  • Reuses the LP data model — well-tested, understood by admins
  • Each cycle is naturally a separate Course instance (clean reporting)
  • Admin gets a review/edit touchpoint each cycle (supports "Reversioned" content)
  • Bounded feature list: 6 specific extensions to LP
  • Lights-out automation can be layered on top in v2

Core Concept: LP as Growing Sequence

LP grows over time — one stable container, many certification cycles
Learning Program: "Annual Security Compliance" Audiences: "All Employees" + Channel-derived audiences (all share ONE schedule) Course: 2025 Jan 1 – Dec 31 ended Course: 2026 Jan 1 – Dec 31 ACTIVE Course: 2027 Jan 1 – Dec 31 future ...

Three Core Use Cases

1
Cohort-Start-Date (Annual Compliance)
Fixed calendar dates for all learners. Example: Annual IT security training, HR policy compliance.

Admin Workflow

1. Create LP + add Course-2026 Start: Jan 1, 2026 | End: Dec 31, 2026 2. Assign audience(s) — learners work on Course-2026 Due: Nov 30 (soft) | End: Dec 31 (hard cancel) 3. "Copy as Next Cycle" Deep-copies Course-2026 → Course-2027 Pre-fills: Start Jan 1, 2027 | End Dec 31, 2027 Admin reviews: update title, add/remove content Repeat step 3 each year...

Learner Timeline

All learners (same clock): Course-2026 (Jan–Dec) Course-2027 (Jan–Dec) LP status for Sam (completed 2026): In Progress Done In Progress Sam's LP stays "complete" until Jan 1, 2027 New hire (Joe joins Jun 2027): 2025 SKIP 2026 SKIP 2027 ACTIVE (6 months left) Rule: skip if assignment_date > end_date End-date cancellation: unfinished learners on Dec 31 → CANCELLED next day
2
Individual-Completion-Date (Badge Renewal)
Each learner's renewal activates N days after THEIR completion. Example: Product certification valid 12 months.

Admin Workflow

1. Create LP + add Initial Certification Start: When assigned | End: None 2. "Copy as Next Cycle" → Renewal 1 Deep-copies Initial Cert → Renewal 1 Set activation: 365 days after completing #1 Admin reviews: update title, adjust content 3. "Copy as Next Cycle" → Renewal 2 Deep-copies Renewal 1 → Renewal 2 Set activation: 365 days after completing #2 Admin reviews: update content if product evolved Repeat as needed (or pre-populate multiple renewals)

Learner Timeline (Per-Learner Clocks)

Alice (completes Mar 1): Initial ✓ Mar 1 365 days Renewal 1 activates Bob (completes Jul 15): Initial ✓ Jul 15 365 days Renewal 1 activates Carol (never completes): Initial — never finishes STALL Renewal never activates Key behaviors: Each learner on own clock • Stall if never completed • Can't renew what you never earned LP completion (dynamic): Alice "complete" after Initial → flips to "in progress" when Renewal 1 activates
3
Individual-Start-Date (Enrollment-Relative)
Each learner's cycles are relative to their enrollment date. Uses existing "Shift relative to enrollment" (isEnrollAtPace). LP level only.

Admin Workflow

1. Create LP with "Shift relative to enrollment" LP setting: isEnrollAtPace = SHIFT_START_DATE (existing feature — makes dates per-learner) 2. Add Course-1 (day 0–365) Start: 0 days after enrollment | End: 365 days after start 3. "Copy as Next Cycle" → Course-2 Deep-copies Course-1 → Course-2 Pre-fills: Start 365 days | End: 365 days after start Repeat for Course-3, Course-4...

Learner Timeline (Shifted Per Enrollment)

Alice (enrolled Jan 1): Course-1: Jan 1 – Dec 31 Course-2: Jan 1 – Dec 31 Bob (enrolled Apr 1): Course-1: Apr 1 – Mar 31 Course-2: Apr 1 – Mar 31 Carol (enrolled Sep 1): Course-1: Sep 1 – Aug 31 Course-2... What makes this work: Existing: startNumDays + "Shift relative to enrollment" New: End date "N days after start" (per-learner hard cancel) Limitation: LP level only (isEnrollAtPace is an LP setting). Standalone courses use Use Case 2.

Six v1 Extensions to LP

New capabilities added to the existing LP model
1. Per-component activation rule ABSOLUTE date or DELAY_FROM_PREVIOUS_COMPLETION 2. Per-component end date with cancellation Hard cancel + revoke access. Preserves data. Doesn't block LP completion. 3. New-assignee skip logic Skip components whose end date passed before learner's assignment date. 4. Unwind LP completion stickiness LP "complete" = computed dynamically from active components. 5. "Copy as Next Cycle" shortcut (all use cases) Deep-copies Course + content items, adds to LP, pre-fills dates. 6. Optional: per-cycle reporting view Behavior Rules Component applies to learner: assignment_date <= end_date (or no end date) Component is active for learner: Applies + activation condition met + end date not passed LP "complete" for learner: All applicable, activated components completed (dynamic) End-date cancellation: Nightly batch on end_date+1. CANCELLED + access revoked. Cancelled components excluded from LP completion calc. Stall on incomplete previous: DELAY rule doesn't fire on cancelled/stalled previous. Due date vs End date: Due = soft (notifications). End = hard (cancel + revoke). Both coexist.

Benefit to Admins After v1

With these six extensions in place, the admin's recurring certification workflow goes from 8 manual steps to 1 manual step (content review):

StepToday (workaround)After v1
1. Copy content items Manual — find each item, copy individually Automated (deep copy creates new entities)
2. Copy the Course Manual — create new Course, add items Automated (new Course + content items in one action)
3. Review/edit content Manual — update title, add/remove items Manual — same (intentional admin touchpoint)
4. Create audience + assign Manual — set up audience, configure dates Eliminated (LP audience persists across cycles)
5. Set start/due/end dates Manual — per audience, each cycle Pre-filled from previous cycle
6. Handle late enrollees Manual — figure out who needs what Automatic (new-assignee skip logic)
7. Cancel expired learners Manual — or doesn't happen at all Automatic (end-date batch cancellation)
8. Track cross-cycle progress Impossible (separate courses, no continuity) Built-in (one LP, all cycles visible)
Net result: Steps 1–2 become one action ("Copy as Next Cycle"). Steps 4–8 are eliminated entirely. Only step 3 (content review) remains manual — and that's intentional for "Reversioned" content where the admin needs to update material each cycle.

Single Schedule Principle

Core rule: The LP schedule (activation rules + end dates) is per-component and LP-wide. It is NOT per-audience. Multiple audiences on an LP are access grants sharing one schedule.

How Multiple Audiences Work

Multiple audiences = multiple access grants, one shared schedule
LP: "Annual Security Compliance" Schedule: Course-2026 (Jan 1 – Dec 31) → Course-2027 (Jan 1 – Dec 31) Audience: "All Employees" Smart Group (direct assign) Audience: from Channel A Auto-created (catalog listing) Audience: from Channel B Auto-created (catalog listing) Audience: "Partners" Smart Group (direct assign) All audiences share the SAME schedule. Audiences only grant access. Channel-derived audiences are auto-created when LP is listed in a Channel. Union-based access via calcChannelShares().

Channel-to-LP Audience Mechanism

When You Need Different Schedules

Different schedules = different LPs. If EMEA needs Jan–Dec cycles and Americas needs Jul–Jun cycles, create two LPs: "Security Compliance — EMEA" and "Security Compliance — Americas". Each has its own schedule and audiences.
ScenarioSolution
Same training, same dates, multiple groupsOne LP, multiple audiences (all share schedule)
LP listed in multiple Channels for catalog browsingOne LP, auto-created audiences per Channel (all share schedule)
Same training but different cycle dates per regionSeparate LPs per region (each with own schedule)
One group needs annual, another needs quarterlySeparate LPs per cadence

"Copy as Next Cycle" — Deep Copy Explained

Available on all use cases via the Actions menu (...) on any LP component. Creates the next cycle with one action.

Why deep copy is required (not optional): Content-item completion is global. If Course-2027 references the same Quiz entity as Course-2026, the system considers it already complete. The new cycle is instantly "done" — defeating the entire purpose. Deep copy creates new content-item entities with their own IDs and empty completion records.
Shallow copy (broken) vs Deep copy (required)
Shallow Copy — BROKEN Course-2026 container Course-2027 new container Same content items (shared refs) Completion: DONE (from 2026) Result: 2027 instantly "complete" Defeats the purpose of recertification Deep Copy — WORKS Course-2026 container Course-2027 new container Quiz-A, Video-B DONE ✓ Quiz-A', Video-B' (new) NOT STARTED Result: 2027 starts fresh New entities, own IDs. Media files shared (not duplicated in storage).

What "Copy as Next Cycle" Does Per Use Case

Use CaseSourceResultPre-filled activation
Cohort-start-date Course-2026 (Jan 1 – Dec 31) Course-2027 (deep copy) ABSOLUTE: Jan 1, 2027. End: Dec 31, 2027.
Individual-completion-date Renewal 1 (365d delay) Renewal 2 (deep copy) DELAY: 365 days after completing previous.
Individual-start-date Course-1 (day 0–365) Course-2 (deep copy) Relative start: day 365. End: 365 days after start.

Deep Copy Scope

Content TypeNew Entity (own ID, empty completion)Shared (same reference)
QuizNew Quiz entity + question structureQuestion media assets
VideoNew Video entity + metadataVideo file (S3/CDN)
DocumentNew Document entity + metadataUnderlying file
SCORM/HTML5New SCORM entity + configSCORM package file
ExerciseNew Exercise entity + rubricRubric templates, media
EventNew Event entity + details

Admin UX: Interactive Prototype

No wizard, no isRecurring flag. Per-component configuration via the existing Manage Collection view. New columns: Start (activation rule) and End (end date). New action: "Copy as next cycle".

Annual Security Compliance
Manage Collection view — showing new Start/End columns and "Copy as next cycle" action
Scenario 1: Cohort-start-date
Scenario 2: Individual-completion-date
Dropdown options reference
Scenario: Annual compliance. All employees must complete current year's course. Admin uses "Copy as next cycle" to create next year.
# Collection Start extended End new Actions
1 Security Compliance 2025 COURSE · ENDED On date: Jan 1, 2025 On date: Dec 31, 2025 ...
2 Security Compliance 2026 COURSE · ACTIVE ...
3 Security Compliance 2027 COURSE · FUTURE ...
New-assignee skip: Joe joins Jun 2027 → skips 2025 and 2026 (end dates passed), sees only 2027.
LP completion: Dynamic. Only currently-active, non-ended components count.
Scenario: Product cert valid 12 months. Each learner's renewal activates 365 days after THEIR completion.
# Collection Start extended End Actions
1 Product Certification - Initial COURSE ...
2 Product Certification - Renewal 1 COURSE 365 days after completing #1 ...
3 Product Certification - Renewal 2 COURSE 365 days after completing #2 ...
Stall semantics: If learner never completes #1, Renewal 1 never activates. Can't renew what you never earned.
Per-learner activation: Joe completes Mar 15 → Renewal activates Mar 15 next year. Jane completes Jul 1 → hers Jul 1 next year.
Reference: All available options. Grey = existing. Green = new in v1.

Start (activation rule)

When assigned (existing)
After: [previous component] (existing, 0-day delay)
On a specific date → [date picker] new
After completing [component] + [N] [days/weeks/months/years] new

End (end date) new column

None (never expires)
On a specific date → [date picker] new
[N] [days/weeks/months] after start new
End date ≠ Due date: Due = soft (notifications only). End = hard (cancels + revokes access).

Actions menu (...) new option

Edit
Remove from program
Move up / Move down
Copy as next cycle new
"Copy as next cycle" deep-copies Course + content items (new entities), appends as next component, pre-fills dates from source.

Key Design Decisions

DecisionResultRationale
ArchitectureLP-sequential with extensionsBounded scope, existing model, admin edit window
Schedule modelSingle schedule per LPAudiences = access grants only. Different schedules → separate LPs.
v1 triggersCohort + Individual-completion + Individual-start (LP only)Covers compliance + badge renewal + enrollment-relative
LP completionComputed dynamicallyDerived, not stored. Flips when new component activates.
New-assignee skipSkip ended components onlyNew hires get current cycle. Rule: assignment_date > end_date → skip.
End-date behaviorHard cancel + revokeDoesn't block LP. Progress preserved for audit.
Stall on no-completionDependent never activatesCan't renew what you never earned.
Copy as Next CycleAll use cases, deep copyNew content-item entities required (completion is global).
BadgesOrthogonal (decoupled in v1)Avoids coupling complexity. Reversible.
Admin UXPer-component config onlyNo wizard, no global flag. v2 adds automation.

Roadmap

v1 — Build Now

  • Per-component activation rule
  • Per-component end date + cancellation
  • New-assignee skip logic
  • Dynamic LP completion
  • Nightly batch (end-date + activation eval)
  • "Copy as Next Cycle" (all use cases)

v2 — Lights-Out

  • Cycle entity (group LP-components)
  • Recurring config + auto-creation
  • Multi-component deep copy
  • Admin notification before activation
  • "Deep Copy a Course" as standalone tool

Future (Unlikely)

  • Per-assignment completion records
  • No content duplication needed
  • True LP-level recurrence
  • The "dragon" — only if required

Out of Scope (v1)

ItemWhy
User-triggered reassignment ("my badge expired, restart")Doesn't fit date-driven model; separate feature
Badge-as-activation-signalCoupling creates inconsistency; decoupled is simpler
Lights-out auto-creationv2 — system auto-creates without admin trigger
Bulk "create N years" wizardv2 — requires cycle entity
Per-audience schedule overridesDifferent schedules = separate LPs (single schedule principle)
Cross-cycle aggregated reportingPer-Course views exist; roll-up is v2
Individual-start-date for standalone CoursesisEnrollAtPace is LP-level only

Open Items

Alternative Approach Considered: "Recurring" Auto-Copy (Approach A)

An alternative design was explored that introduces a new "Recurring" modality on LP/Course. In this approach, the system owns the schedule and automatically creates copies of the LP/Course on a cadence, managing ACTIVE and NEXT-ACTIVE states.

How Approach A Works

Why LP-Sequential (This Proposal) Is Preferred

DimensionThis proposal (LP-Sequential)Alternative (Auto-Copy)
Scope 6 bounded extensions to existing LP model Open-ended: state machine, deep-copy, 3 sub-modes, badge integration
Data model Reuses existing LP structure New entities: Recurring config, ACTIVE/NEXT-ACTIVE states, copy metadata
Content review Admin has natural touchpoint each cycle — matches "Reversioned" need Auto-copy happens silently; admin notification is an added feature
Audience overlap Single schedule principle — clean and simple Multiple audiences with different schedules unsolved
Badge renewal Handled by DELAY_FROM_PREVIOUS_COMPLETION (per-learner) Requires most complex "Relative" variant + badge expiration coupling
Reporting Natural: each Course in LP = one cycle. One LP = full history. Requires new cross-copy aggregation (copies are separate LPs or separate entities)
Risk Lower blast radius — extensions are additive Deep-copy of LP is a feature on its own; state machine adds failure modes
Path to automation v2 adds lights-out on top of v1 primitives Automation is built-in from day 1 (but all complexity ships together)
Summary: The Auto-Copy approach solves the same problems but hides complexity inside automated machinery. The hard problems (audience overlap, badge renewal, content versioning) aren't actually solved better — they're just less visible. LP-Sequential keeps the complexity explicit and bounded, ships faster, and doesn't foreclose adding automation later. The one genuine advantage of Auto-Copy — lights-out scheduling — becomes v2 layered on top of v1's primitives.