I want to share with you a template that I have been generating for my exams. I have been using Typst to generate the exams and I have been creating a problem bank with the problems that I have been using in my exams. This way, I can reuse the problems and generate exams with selected problems.
You will see a lot of errors in the template code, a new version is still in development and it has lot of improvements. I will update the template code when the new version is released.
At the end of the article, you will find a simplification of the new version of the template code. But for now you can use the current version to generate your exams.
The next template is avaliable at QG/Typst.
Quiz Generator in Typst. Create multiple quiz versions from the same template.
| Questionarie | Solutions |
|---|---|
![]() | ![]() |
Information

Modify the questionarie object with your own data.
#while i < students {
...
show: questionarie.with(
teacher: "Oswaldo Arias Estrada",
title: "1st Partial Exam",
subject: "Vectorial Calculus",
school: "Instituto Politénico Nacional",
faculty: "Unidad Profesional Interdisciplinaria \n de Ingeniería Campus Guanajuato",
academy: "Mathematics",
class: "1AV1",
date: "06/01/25",
paper: "us-letter",
textFont: "Atkinson Hyperlegible",
codeFont: "Roboto Mono",
schoolLogo: "images/IPN.png",
schoolLogoWidth: 30%,
facultyLogo: "images/UPIIG.png",
facultyLogoWidth: 40%,
instructions: [
- Answer the following questions.
- You have 1 hour to complete the exam.
- Write your answers on the exam paper.
],
...
)
...
}
Versions
The Exam ID is generated using the seed, the examVersions, and the number of student. It appears in the information section and in the header of each page. The first part of the Exam ID is the seed, the second part is the version and the third part is the student number.

Modify the seed, students, examVersions, and answers variables with your own data.
#let seed = 129302183
#let students = 12
#let examVersions = 4
#let answers = true
Questions
Modify the questions object with your own. The questions object is a tuple with the path to the questions files.
#let questions = (
"questions/hypotenuse of a right triangle.typ",
"questions/balls.typ",
"questions/derivative.typ",
"questions/cars speeds.typ",
"questions/polygons.typ"
)
Question File
Question

The question function is a tuple with the seed to generate the random numbers to use in the question. It also has the question text.
#let question(seed) = {
let rng = gen-rng(seed)
let blue = 0
let yellow = 0
let red = 0
let color = 0
let colorName = ""
let numberOfBalls = 0
(rng, blue) = integers(rng, low: 20, high: 100)
(rng, yellow) = integers(rng, low: 30, high: 100)
(rng, red) = integers(rng, low: 1, high: 50)
let balls = (
blue: blue,
yellow: yellow,
red: red,
)
(rng, color) = integers(rng, low: 1, high: 3, endpoint: true)
if color == 1 {
colorName = "blue"
numberOfBalls = yellow + red + 1
} else if color == 2 {
colorName = "yellow"
numberOfBalls = blue + red + 1
} else {
colorName = "red"
numberOfBalls = blue + yellow + 1
}
[A bag has #balls.blue blue balls, #balls.yellow yellow balls, and #balls.red red balls. How many random balls must be drawn from the bag to ensure that at least one #colorName ball is drawn?]
}
Points and Size
The questionPoints and answerSize variables are used to set the points for each question and the size of the answer space.
#let questionPoints = 15
#let answerSize = 8
Answer

The answer function is a tuple with the seed to generate the random numbers to use in the answer. It also has the answer text.
#let answer(seed) = {
let rng = gen-rng(seed)
let blue = 0
let yellow = 0
let red = 0
let color = 0
let colorName = ""
let numberOfBalls = 0
(rng, blue) = integers(rng, low: 20, high: 100)
(rng, yellow) = integers(rng, low: 30, high: 100)
(rng, red) = integers(rng, low: 1, high: 50)
let balls = (
blue: blue,
yellow: yellow,
red: red,
)
(rng, color) = integers(rng, low: 1, high: 3, endpoint: true)
if color == 1 {
colorName = "blue"
numberOfBalls = yellow + red + 1
} else if color == 2 {
colorName = "yellow"
numberOfBalls = blue + red + 1
} else {
colorName = "red"
numberOfBalls = blue + yellow + 1
}
[To ensure that at least one #colorName ball is drawn, we must draw at least #numberOfBalls balls.]
}
New version

exam.typ
The main file has been simplified, and contains everything needed to generate the exam.
#import "../template.typ": *
#show: exam.with(
title: "2008 - Secundaria 1ero - Selectivo 1",
author: "Oswaldo Arias Estrada",
place: "Guanajuato",
year: "2008",
date: "2008-11-01",
level: "Secundaria",
grade: "1",
selective: "1",
leftLogo: "images/logos/CIMAT.png",
rightLogo: "images/logos/UG.png",
instructions: [
+ Lee cuidadosamente cada uno de los problemas.
+ Resuelve los problemas en las hojas que se te darán. Para que una respuesta esté completa es necesario que expliques el procedimiento que utilizaste para resolverlo.
+ Solamente está permitido el uso de lápiz, sacapuntas, pluma, borrador y juego de geometría.
+ Los resultados serán informados vía telefónica a las escuelas de los participantes.
+ Los alumnos seleccionados serán invitados al entrenamiento que se llevará a cabo los próximos 7 y 8 de marzo, y al selectivo final de esta olimpiada el cual tendrá lugar el día 15 de marzo en la ciudad de Guanajuato.
],
questions: (
"Problemas/Paulina y Veronica.typ",
"Problemas/Una senora tiene una cierta cantidad de dulces.typ",
),
answers: true,
)
template.typ
The template has been extended so that it generates the exam with the information provided in the main file.
#let exam(
title: "",
author: "",
place: "",
year: "",
date: "",
level: "",
grade: "",
selective: "",
leftLogo: "",
rightLogo: "",
instructions: "",
questions: [],
answers: false,
body,
) = {
...
align(center)[
#table(columns: (7), align: center+horizon, inset: 10pt,
table.cell(rowspan: 4, colspan: 2, )[#image(leftLogo, width: 1in)], [*Lugar*], [*Año*], [*Fecha*], table.cell(rowspan: 4, colspan: 2, )[#image(rightLogo,width: 1in)],
[#place], [#year], [#date],
[*Nivel*], [*Grado*], [*Selectivo*],
[#level], [#grade], [#selective],
)
]
v(1em)
if instructions != "" [
#text(size: 16pt, weight: "bold")[Instrucciones.]
#v(1em)
#instructions
#v(1em)
]
text(size: 16pt, weight: "bold")[Problemas.]
let i = 0
while i < questions.len() {
import questions.at(i): question, answer
align(center)[#line(length: 100%, stroke: (paint: gray, thickness: 1pt, dash: "dashed"))]
enum.item(1 + i)[
#question()
#v(1em)
#if answers {
[*Respuesta.* \ #answer()]
}
#v(1em)
]
i = i + 1
}
body
}
Thank you for reading this article. I hope you find it useful. If you have any questions, please reach out to me on GitHub.

