Generate a quiz bank from source material
Turn a Northwind training Doc into exam questions automatically — multiple choice with answers.
Published Nov 27, 2025
Northwind’s training material gets written once and then it just sits there. Turning a process guide or an onboarding Doc into a quiz — the thing that actually checks whether anyone absorbed it — is its own chore: write the question, invent three plausible wrong answers, mark the right one, repeat twenty times. Most teams never get round to it.
This script reads a source Doc and asks Claude to write multiple-choice questions straight from the content — each with four options, a marked correct answer, and a one-line explanation. It appends every question as a row in a quiz spreadsheet you can use as a question bank or import into a quiz tool.
What you’ll need
- A Google Doc holding the source material — a training guide, a policy, an onboarding handbook.
- A Google Sheet to collect the questions — the script appends to the first tab, one question per row.
- An Anthropic API key saved as
ANTHROPIC_API_KEYin Script Properties — see Store API keys and secrets securely.
The script
// The Doc to read questions from and the Sheet to collect them in.
const SOURCE_DOC_ID = '1abcSourceDocId';
const QUIZ_SHEET_ID = '1abcQuizId';
// How many questions to generate per run.
const QUESTION_COUNT = 10;
// How much of the Doc to send in one pass — keeps the prompt within a
// sensible token budget. See "Watch out for".
const CONTEXT_CHARS = 8000;
/**
* Reads the source Doc, asks Claude to write multiple-choice
* questions, and appends each one as a row in the quiz spreadsheet.
*/
function generateQuiz(count = QUESTION_COUNT) {
// 1. Read the source material.
const text = DocumentApp.openById(SOURCE_DOC_ID)
.getBody().getText().slice(0, CONTEXT_CHARS);
if (!text.trim()) {
Logger.log('Source Doc is empty — nothing to generate.');
return;
}
// 2. Ask Claude for questions as strict JSON. A fixed schema is the
// difference between a parseable result and a parsing nightmare.
const prompt =
'Generate ' + count + ' multiple-choice questions from this ' +
'Northwind material. Return ONLY a JSON array — no prose, no ' +
'markdown — in this shape: [{"question": string, ' +
'"options": [4 strings], "correctIndex": 0-3, "explain": string}]. ' +
'Base every question on the text; do not invent facts.\n\n' + text;
const qs = JSON.parse(stripFences(
callClaude(prompt, 'claude-sonnet-4-6', 3000)));
if (!qs.length) {
Logger.log('No questions returned — nothing to write.');
return;
}
// 3. Append one row per question: prompt, four options, the correct
// answer text, and the explanation.
const sheet = SpreadsheetApp.openById(QUIZ_SHEET_ID).getSheets()[0];
for (const q of qs) {
sheet.appendRow([
q.question, ...q.options, q.options[q.correctIndex], q.explain,
]);
}
Logger.log('Appended ' + qs.length + ' questions to the quiz sheet.');
}
/**
* Claude occasionally wraps JSON in a ```json code fence. Strip it so
* JSON.parse never chokes on the markdown.
*/
function stripFences(textValue) {
return textValue.replace(/```(?:json)?/g, '').trim();
}
/**
* Minimal Anthropic API call. The key lives in Script Properties — it
* is never pasted into the code.
*/
function callClaude(prompt, model = 'claude-haiku-4-5-20251001', maxTokens = 400) {
const key = PropertiesService.getScriptProperties()
.getProperty('ANTHROPIC_API_KEY');
const res = UrlFetchApp.fetch('https://api.anthropic.com/v1/messages', {
method: 'post',
contentType: 'application/json',
headers: { 'x-api-key': key, 'anthropic-version': '2023-06-01' },
payload: JSON.stringify({
model,
max_tokens: maxTokens,
messages: [{ role: 'user', content: prompt }],
}),
muteHttpExceptions: true,
});
return JSON.parse(res.getContentText()).content[0].text.trim();
}
How it works
generateQuizopens the source Doc and reads the firstCONTEXT_CHARScharacters of its text.- If the Doc is empty, it logs a message and stops — no wasted API call.
- It builds a prompt that pins the output to a strict JSON schema: an array of
objects, each with a
question, fouroptions, acorrectIndex, and anexplainline. - It calls Claude Sonnet, which is worth the extra cost here because writing
plausible wrong answers — distractors — needs real reasoning over the
material.
stripFencesremoves any code fence, thenJSON.parseparses it. - If no questions come back, it logs a message and stops.
- It appends one row per question to the first tab of the quiz sheet, spreading
the four options into their own columns and resolving
correctIndexto the answer text.
Example run
Say the source Doc explains Northwind’s refund policy. With QUESTION_COUNT at
its default of 10, a run appends rows like this (one shown):
| Question | Option A | Option B | Option C | Option D | Correct answer | Explanation |
|---|---|---|---|---|---|---|
| Within how many days can a Northwind customer request a refund? | 7 days | 14 days | 30 days | 90 days | 30 days | The policy allows refunds within 30 days of purchase. |
Ten rows like that, generated straight from the Doc — a ready question bank instead of a blank template.
Run it
This is an on-demand job — run it when training material is updated:
- Set
SOURCE_DOC_IDto the material andQUIZ_SHEET_IDto the question bank. - In the Apps Script editor, select
generateQuizand click Run. - Approve the authorisation prompt the first time.
- Open the quiz sheet to review the new rows.
To make it self-serve, add a custom menu on the quiz sheet:
function onOpen() {
SpreadsheetApp.getUi()
.createMenu('Quiz tools')
.addItem('Generate questions', 'generateQuiz')
.addToUi();
}
Watch out for
- Always review the questions before using them. Claude can write a question whose “correct” answer is debatable, or whose distractors are too obviously wrong — generation is a draft, not a final exam.
- Run it twice and you get a second batch of questions appended below the first, often overlapping. Clear the sheet between runs, or de-duplicate afterwards.
- Only the first
CONTEXT_CHARScharacters are read. For a long handbook, run it over each section so questions cover the whole document, not just the start. max_tokensis set to 3000 for ten questions. Ask for many more and the reply can be cut off mid-JSON — raisemax_tokensin step withcount.- Source material is sent to the API. Keep confidential internal documents off this script, or only point it at material cleared for wider use.
Related
Generate and test email subject lines
A/B test AI-written Northwind subject lines for open rate — outputs ranked by past performance.
Updated Mar 3, 2026
Build retrieval-augmented Q&A over your data
Answer Northwind questions grounded in your own Sheet data — pass relevant rows as context.
Updated Feb 27, 2026
Build an AI weekly-report narrator
Turn Northwind metrics into a written executive summary — numbers in, prose out.
Updated Feb 23, 2026
Build a multi-step AI agent workflow
Chain Claude prompts to complete a Northwind task end to end — research → draft → critique → finalise.
Updated Feb 11, 2026
Adapt marketing copy per region
Localise Northwind tone and references by market with AI — same message, regional flavour.
Updated Jan 30, 2026