- Get Started
- Localhost
- Architecture
- Structure
- Pattern
- Page
- Layout
- Content
- Types
- Config
- Actions
- Queries
- Authorization
- Validation
- Form
- Table
- Detail
- Card
- Util
- Hooks
- List Params
- Views
- README.md
- ISSUE.md
- Technology Stack
- Database
- File
- CDN Assets
- Entry Points
- Dashboard
- Authentication
- Credentials
- OAuth
- Flow Diagrams
- Multi-Tenancy
- Offline
- Onboarding
- Onboarding Videos
- Add Values
- Admission
- Application
- Attendance
- Compliance
- Profile
- Exams
- Exam Wizard
- Timetable
- Classrooms
- Notifications
- Conference
- LMS (Lumos)
- Finance
- Fee Management
- Invoice
- Wallet
- Salary
- Payroll
- Timesheet
- Expenses
- Budget
- Receipt
- Accounts
- Banking
- Reports
- Dashboard
- Permissions
- Messages
- Integration Flow
- Provision
- AI Document Processing
- Document Intelligence
- Internationalization
- Translation
- Translation Guide
- Icons
- Docs Factory
- Inspiration
- Listings
- Teachers
- Students
- Catalog
- Library
- Contributing
- Code of conduct
- GitHub Workflow
- Database Seeds
- Database Safety
- Test Accounts
- Bulk import
- Playwright
- Prettier
- Block Rebound
- Transportation
The exams module covers the full lifecycle: question authoring, paper printing, digital taking, AI-assisted marking, analytics, certificates, and progress tracking. 18 sub-modules, 40+ routes, ~80 component files.
On a phone
Below the md breakpoint the exams overview is composed per role — a banner for the next exam, a figures panel, app-tile doors, and dated rows for upcoming exams and results — and the authoring pages (generate, catalog, question bank, certificates) show grey cards two across with pill buttons. Tablets and desktops are unchanged; the exams data tables stay tables.
Sub-modules
| Sub-module | Purpose |
|---|---|
| Question bank | Store and manage questions by subject, type, difficulty, Bloom level |
| Psychometrics | Item analysis, discrimination index, distractor effectiveness |
| Templates | Reusable blueprints with question distribution rules |
| Catalog | Browse and import shared exam templates |
| Blueprints | Regional exam structures (national presets) |
| Auto-generation | Generate exams from templates with smart selection |
| Versions | Multiple versions with shuffled questions and contribution tracking |
| AI question gen | Generate questions from curriculum (beta) |
| Management | CRUD, scheduling, conflict detection, analytics |
| Paper generation | PDF papers with composable template system |
| Composition | Variant-based slot composition |
| Regional presets | Country-specific formatting (SD, SA, US, MENA) |
| Digital taking | Online exam player with proctoring |
| Quick exams | Lightweight teacher quizzes with instant take |
| Auto-marking | MCQ, T/F, fill-in-blank |
| AI grading | Essays and short answers |
| OCR | Handwritten answers via GPT-4 Vision |
| Bulk marking | Grade entire exams at once, question-based view |
| Results & analytics | Performance analysis, batch PDF, CSV export |
| Certificates | Configurable templates with QR verification |
| Grading | Boundaries, CGPA, retakes |
| Progress | Schedules and monitoring |
| Mock exams | Practice exams |
| Quiz games | Gamified quizzes |
Question bank
7 question types, 4 auto-gradable, 2 AI-assisted.
| Type | Auto-gradable | Template |
|---|---|---|
| Multiple choice | Y | templates/multiple-choice/ |
| True / false | Y | templates/true-false/ |
| Fill in blank | Y | templates/fill-blank/ |
| Short answer | AI-assisted | templates/short-answer/ |
| Essay | AI-assisted | templates/essay/ |
| Matching | Y | templates/matching/ |
| Ordering | Y | templates/ordering/ |
qbank/analytics/ provides discrimination index, distractor analysis, difficulty calibration, success rate, usage frequency. qbank/catalog-tab.tsx browses shared questions.
CSV bulk import:
questionText,questionType,difficulty,bloomLevel,points,options,sampleAnswer,tags
"What is 2+2?",MULTIPLE_CHOICE,EASY,REMEMBER,1,"[{""text"":""4"",""isCorrect"":true},{""text"":""3"",""isCorrect"":false}]","","math,arithmetic"Create an exam — two modes
The front door (/exams/new) is a single screen offering two creation modes. Both converge on the same downstream — a GeneratedExam with questions → paper preview → schedule — and differ only in how the blueprint and questions are seeded.
| Mode | Route | Seeds from | Speed |
|---|---|---|---|
| Adopt a template | /exams/generate/catalog | A platform-curated, curriculum-aligned ExamTemplate (browse → preview → Adopt) | Fastest |
| Generate with AI | /exams/qbank/ai-generate | A topic prompt → AI-drafted questions into your bank (review before saving) | Fast |
The in-app "build a template from primitives" wizard was removed — a paper's layout is now an uploaded .docx (see Document templates below), while its question distribution lives on a SchoolExamTemplate blueprint seeded by adopting a catalog blueprint.
Document templates
Schools print on their own mandated stationery, so the paper's layout is a .docx the school uploads — not something built in-app. /exams/templates (staff only: DEVELOPER / ADMIN / TEACHER) manages two categories, Exam papers and Certificates; report-card layouts live at /grades/templates. There is no standalone /documents page.
Author the Word file with {{tag}} placeholders and {{#loop}}…{{/loop}} sections, upload it, and the merge fields are detected automatically and shown back to you. The full vocabulary per category is listed in the upload dialog.
You do not have to start from a blank page: Starter template hands you a real .docx for that category with every tag already correct — edit the styling, keep the tags. Use it.
What the upload checks
Every one of these failures used to surface only once a school printed the paper, so the upload now inspects the file and tells you which it is:
| What is wrong | What happens |
|---|---|
| A section is opened but never closed (or closed but never opened) | Refused — it can never fill. The offending tag is named so you can fix it in Word and upload again |
Single braces: {#questions} instead of {{#questions}} | Saved, with a warning listing each marker. They print into the document as text and their contents are dropped |
A tag this category has no data for — usually a typo, e.g. {{schoolNmae}} | Saved, with that tag flagged in the detected list. It fills blank |
| No tags at all | Saved, with a warning. Filling it returns an unchanged copy of your file |
Only the first is rejected; the rest are stored so you can still use the template if the warning is a false alarm.
Filling an exam paper
An uploaded exam-paper template is bound to real exam data through Use template, which offers two sources:
| Source | What you pick | What happens |
|---|---|---|
| An existing exam | A GeneratedExam that already has questions | Fills your layout directly |
| Build one now | A blueprint + grade (one section or all) + paper title + date + exam type | Creates the SchoolExam + GeneratedExam, auto-selects questions from the bank per the blueprint, fills |
Before generating, the dialog shows coverage — every tag detected in your template, marked as either fillable or unsupported — so a mismatch is visible before you download rather than as a blank in the printed paper. Output is .docx; PDF conversion is deferred.
Two ways to lay out the body
The resolver always supplies both, so a template uses whichever fits and the unused one costs nothing:
| Loop | Shape |
|---|---|
{{#questions}}…{{/questions}} | One flat, continuously numbered list |
{{#sections}}…{{/sections}} | The same questions grouped by type — "1. Multiple choice", "2. Essay" — each with its own mark total |
Inside a section, questions keep the paper-wide {{order}} and also carry {{numberInSection}}, so a sectioned paper and a flat one number identically. Each question exposes {{typeLabel}}, the conditional {{#hasOptions}} / {{#isMcq}} blocks, and {{#answerLines}} — blank ruled writing space sized by question type (essay 8 lines, short answer 3, fill-in-the-blank 1, none for multiple choice). The starter template demonstrates all of it.
When the bank is short
Question selection degrades rather than fails: it fills every distribution slot the bank can cover and records the rest. A blueprint asking for six hard essays against a bank holding none still produces a paper — a short one. The dialog stays open after the download and names the unfilled categories, because the alternative is a teacher discovering a 12-mark paper for a 50-mark exam in the exam hall. Generation only fails outright when the bank yields no question at all, and in that case the newly created exam is rolled back rather than left empty on the schedule.
Generation
| Route | Purpose |
|---|---|
/exams/generate/templates | Distribution rules per type, difficulty, Bloom |
/exams/generate/catalog | Browse and import shared templates |
/exams/qbank/ai-generate | AI-drafted questions from a topic prompt |
/exams/generate/versions | Multiple shuffled versions |
/exams/generate/contributions | Author tracking |
/exams/templates | Your own .docx layouts (upload + fill) |
Smart selection: group by type and difficulty, fill distribution, apply Bloom distribution, seeded randomization, prevent duplicates across versions.
Management
| Route | Purpose |
|---|---|
/exams/new | Create-exam chooser (adopt · AI · from scratch) |
/exams/[id] | Details + result summary |
/exams/[id]/edit | Modify before administration |
manage/calendar.tsx — visual schedule. manage/conflict-display.tsx — overlap warnings. manage/analytics-dashboard.tsx — school-wide stats, standards coverage, quick paper-gen, export.
The /exams/manage list (manage/columns.tsx) shows seven columns: Title (row anchor), Grade (the exam's section, else its grade), Subject, Type, When (date · start time · duration, merged into one), Marks, and Status. An exam is set for a grade — one section of it or the whole grade — and a subject;
Paper generation
@react-pdf/renderer-based system with a four-layer composable template:
-
Atoms (13 in
templates/atom/) —alignment-marker,answer-line,blank-slot,bubble,checkbox,divider,field-line,logo-block,option-label,points-badge,question-number,type-label,watermark. -
Question sections — one per question type.
-
Page sections — 6 slots × variants:
Slot Variants Header standard, ministry, minimal, bilingual, centered Footer standard, disclaimer, minimal, grading Student info standard, bubble-id, table, photo Instructions standard, compact, rules, sectioned Answer sheet standard, omr, grid Cover standard, toc, ministry -
Composition —
ComposableDocumentreadsCompositionConfigand assembles slot variants.
interface CompositionConfig {
slots: Record<
| "header"
| "footer"
| "studentInfo"
| "instructions"
| "answerSheet"
| "cover",
string
>
decorations: {
accentBar: {
enabled: boolean
height?: number
colorKey?: "accent" | "primary"
}
watermark: { enabled: boolean; text?: string; opacity?: number }
frame: { enabled: boolean; outerWidth?: number; innerWidth?: number }
}
slotProps: Record<string, unknown>
}Resolution: blockConfig JSON → regional preset overrides → DEFAULT_COMPOSITION.
paper/composition-panel.tsx provides the variant picker. The four legacy templates (Classic, Modern, Formal, Custom) are deprecated wrappers around ComposableDocument.
| Preset | Highlights |
|---|---|
sd-national | Sudan — ministry header, disclaimer, watermark |
sa-national | Saudi Arabia — ministry, bubble-id, OMR sheets |
us-standard | USA standard layout |
mena-private | MENA private — bilingual header, custom branding |
Each style has a PaperTheme with locale-aware colour, typography, spacing, RTL/LTR direction, and number style (Arabic / Latin).
Digital taking
take/exam-player.tsx is the full-screen runner: per-question nav, countdown timer, auto-save, validated submit. take/proctor-guard.tsx adds device fingerprint, security flag reporting (tab switch, copy / paste, screen capture), retake-penalty calculation.
startExamSession(input: StartExamSessionInput)
autoSaveAnswers(input: AutoSaveAnswersInput)
reportSecurityFlag(input: ReportSecurityFlagInput)
submitExamAnswers(input: SubmitExamAnswersInput)Quick assessments and progress reports
A quick assessment (exit ticket, poll, warm-up, check-in) is set for an audience like an exam: a grade, one of its sections or the whole grade, and a subject, picked with the shared TeachingScopePicker and checked by resolveTeachingScope. Only teachers and admins write, launch and close them; a student sees and answers only their audience's, and an identified answer lands in the gradebook under the student's section, grade and term. Assessments made before classes were retired keep their class.
A progress-report schedule covers a section, a whole grade, or — with neither — the whole school; deleting the grade or section deletes its schedule rather than widening it. Only teachers and admins manage schedules or run them.
Marking
NOT_STARTED → AUTO_GRADED | AI_GRADED | IN_PROGRESS → REVIEWED → COMPLETED. Override supported at any point.
autoGradeAnswer(studentAnswerId: string)
aiGradeAnswer(studentAnswerId: string)
manualGrade(answerId: string, points: number, feedback?: string)
bulkGradeExam(examId: string, method: GradingMethod)
overrideGrade(resultId: string, newScore: number, reason: string)OCR uses GPT-4 Vision for handwritten answers. AI grading uses essay rubrics from the question bank.
Results & analytics
/exams/result lists exams with class average, pass rate, grade distribution. Per-exam at /exams/result/[id]/analytics:
- Class — average, median, mode, std dev, top / bottom performers, distribution chart, trend.
- Question — discrimination index, distractor analysis, most missed, average time per question.
Batch PDF at /exams/result/[id]/pdf/batch — background processing, multiple result-card templates, ZIP. CSV export for raw data. results/cache-manager.ts caches computed results.
Certificates
| Step | Surface |
|---|---|
| Configs | /exams/certificates/configs/new, /configs/[id] |
| Generate (single) | per-student form |
| Generate (batch) | batch action over a class |
| QR verification | public at /exams/certificates/verify/[code] |
| Revocation | with reason tracking |
| Share | shareable link |
createCertificateConfig(input)
updateCertificateConfig(id, input)
generateCertificate(input)
batchGenerateCertificates(input)
verifyCertificate(code) // public
revokeCertificate(id, reason)
shareCertificate(id)Server actions
| Module | Path | Surface |
|---|---|---|
| Question analytics | qbank/analytics/actions.ts | getQuestionAnalytics, getQuestionPsychometrics |
| Generation | generate/actions.ts | createTemplate, updateTemplate, deleteTemplate, generateExam, previewExamGeneration, regenerateExam |
| Management | manage/actions.ts | createExam, updateExam, deleteExam, getExams, getExamDetails |
| Paper | paper/actions/ | createPaperConfig, updatePaperConfig, generatePaperPDF |
| Marking | mark/actions.ts | see above |
| Results | results/actions.ts | getExamResults, getExamAnalytics, generateSinglePDF, generateBatchPDFs, exportResultsToCSV |
| Quick assessments | quick/actions/ | createQuickAssessment, getQuickAssessments, launchQuickAssessment, submitQuickResponse, getQuickAssessmentResults |
| Grading | grading/actions.ts | getGradeBoundaries, updateGradeBoundaries, calculateCGPA, manageRetake |
| Progress | progress/actions.ts | createProgressSchedule, getProgressSchedules, updateProgressSchedule, generateProgressReports |
Seed
pnpm db:seed:single exams # 70+ questions, 8 templates, 27 sample exams
pnpm db:seed:single qbank # extended question bank + psychometrics
pnpm db:seed:single exam-templates # shared catalog templates
pnpm db:seed:single exam-blueprints # regional structures (SD, SA, …)Troubleshooting
| Symptom | Likely cause |
|---|---|
| Exam generation fails | Pool too small for distribution; fix template or add questions |
| AI grading errors | OPENAI_API_KEY missing, rate limit, missing rubric |
| PDF paper crashes | @react-pdf/renderer missing; missing logo URL; Rubik font unreachable; insufficient memory for batch |
| Digital exam won't start | Exam status not IN_PROGRESS; student not enrolled |
| Proctor guard inactive | Production HTTPS required |
| Certificate QR doesn't scan | QR_CODE_SECRET missing; certificate revoked; verify route blocked |
Automation pipeline
The scoring loop is largely hands-off once questions are in the bank. Objective exams need zero manual marking; exams with subjective questions take one click.
1. Auto-generate from the bank (exam-wizard-v2 → "Auto-generate from template")
└─ autoGenerateExamQuestions() selects questions per the template distribution,
assigns per-question points, writes GeneratedExamQuestion rows, drops the
stale answer key. (Adopting a catalog exam does the same in one transaction.)
2. Exam opens on schedule (/api/cron/exam-lifecycle, */15)
└─ PLANNED → IN_PROGRESS at the scheduled start; IN_PROGRESS → COMPLETED after
the end window (examDate + endTime + lateSubmitMinutes).
3. Students take + submit (take route → ExamPlayer)
└─ getExamForPlayer() loads the proctored runner, resolving the real Student.id
and stripping isCorrect from options. submitExamSession() instant-grades a
fully-objective paper on submit → finalizeStudentExam() → ExamResult + Result.
4. Finalize exams with subjective questions (one click)
└─ "Auto-mark & publish" → finalizeExamResults({ aiGradeSubjective: true,
publish: true }): objective auto-mark + AI-grade essay/short-answer, aggregate
MarkingResult → ExamResult (analytics, certificates) + Result (gradebook),
notify students.
5. Report cards auto-generate at term end (/api/cron/term-end-report-cards, daily)
└─ generateReportCardsCore() aggregates each term's results into ReportCard +
ReportCardGrade (dedup by examId), plus credit-weighted GPA, rank and
attendance days. Set-based: the whole cohort in a fixed handful of queries
(~1s for 970 students), so it finishes inside a cron/action budget.
Drafts only — does not publish.
6. Admin reviews + publishes
└─ publishReportCards() → notifies guardians/students; process-report-card-pdfs
cron renders the PDF out-of-band → S3 → pdfUrl.
7. Official documents (documents block)
└─ Upload a .docx template (EXAM_PAPER / CERTIFICATE / REPORT_CARD) — or start from
the built-in Starter template — and fill it with real data via "Generate (my
template)" (generateFromDefaultTemplate). On /grades/reports, "Generate all"
fills the whole filtered term at once (generateFromDefaultTemplateBulk),
chunked 50 cards per zip.
8. Reminders (/api/cron/exam-reminders)
└─ sweeps upcoming exams, dispatches advance notifications to students + teachers.
Key invariant: grades/lib/gradebook.ts is the single write path for ALL automated scoring surfaces (exams, quick assessments, lumos lesson quizzes). It is not "use server" — import it from server actions.
Deferred: a fully session-less AI-finalize-at-close cron. The marking stack (getOrCreateAnswerKey, batchAutoGradeWithKey, batchAIGrade) derives schoolId from auth(), so a timer can't drive it until cron-callable cores are extracted — until then, subjective exams finalize with the one-click "Auto-mark & publish" button.
See also
- Exam wizard — template + generation + cert wizards
- Competitors → assessment tools — the ~25 exam-creation tools that shaped the 3-mode front door
- Multi-tenancy — RBAC + scoping
On This Page
On a phoneSub-modulesQuestion bankCreate an exam — two modesDocument templatesWhat the upload checksFilling an exam paperTwo ways to lay out the bodyWhen the bank is shortGenerationManagementPaper generationDigital takingQuick assessments and progress reportsMarkingResults & analyticsCertificatesServer actionsSeedTroubleshootingAutomation pipelineSee also