This workflow corresponds to n8n.io template #13599 — we link there as the canonical source.
This workflow follows the OpenAI → Telegram recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "E4rYEKgHQnMvf69J",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "ExamForge AI \u2013 PDF to Structured Exam Generator (n8n Workflow)",
"tags": [],
"nodes": [
{
"id": "cffb63a4-8e42-437e-bd9c-1d6a16e5eeee",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [
-160,
64
],
"parameters": {
"path": "cd37ea38-9ff5-4518-a17d-5f443ef4ad3b",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "0ba4e4b7-a5da-46cf-aeb9-07567ed28be2",
"name": "Extract from File",
"type": "n8n-nodes-base.extractFromFile",
"position": [
496,
48
],
"parameters": {
"options": {},
"operation": "pdf",
"binaryPropertyName": "={{ $json.data }}"
},
"typeVersion": 1.1
},
{
"id": "8c9dc848-766e-4e10-8bcb-f873a2b6812a",
"name": "Message a model",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
96,
656
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {},
"responses": {
"values": [
{
"content": "=You are an expert assessment designer.\nGenerate structured exam questions based strictly on the provided material.\nReturn ONLY valid JSON with this format:\n\n{\n\u201cmcq\u201d: [\n{\n\u201cquestion\u201d: \u201c\u201d,\n\u201coptions\u201d: {\n\u201cA\u201d: \u201c\u201d,\n\u201cB\u201d: \u201c\u201d,\n\u201cC\u201d: \u201c\u201d,\n\u201cD\u201d: \u201c\u201d\n},\n\u201canswer\u201d: \u201cA\u201d,\n\u201cdifficulty\u201d: \u201c\u201d\n}\n],\n\u201cessay\u201d: [\n{\n\u201cquestion\u201d: \u201c\u201d,\n\u201cexpected_points\u201d: \u201c\u201d,\n\u201cdifficulty\u201d: \u201c\u201d\n}\n]\n}\n\nRules:\n\t\u2022\tQuestions must strictly follow the requested difficulty level.\n\t\u2022\tAvoid generic or vague questions.\n\t\u2022\tDo not include explanations outside JSON.\n\n\u2e3b\n\nUSER PROMPT dynamic:\n\nMaterial:\n{{ $json.cleaned_text }}\nGenerate:\n\t\u2022\t{{ $('Webhook').item.json.body.mcq_count }} multiple choice questions\n\t\u2022\t{{ $('Webhook').item.json.body.essay_count }} essay questions\nDifficulty: {{ $('Webhook').item.json.body.difficulty }}\nLanguage: {{ $('Webhook').item.json.body.language }}"
}
]
},
"builtInTools": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "fed1bbb3-7548-47eb-8f1b-da99de9be2b6",
"name": "Parse JSON",
"type": "n8n-nodes-base.code",
"position": [
480,
656
],
"parameters": {
"jsCode": "\nlet raw = $input.first().json.output[0].content[0].text\n//let raw = $json.choices?.[0]?.message?.content || \"\";\n\n// Hapus markdown code block jika ada\nraw = raw.replace(/```json/g, \"\")\n .replace(/```/g, \"\")\n .trim();\n\nlet parsed;\n\ntry {\n parsed = JSON.parse(raw);\n} catch (error) {\n throw new Error(\"Invalid JSON from OpenAI. Please check prompt structure.\");\n}\n\nreturn [\n {\n json: parsed\n }\n];"
},
"typeVersion": 2
},
{
"id": "273dfa0f-5e1c-47b1-985c-235da64a6463",
"name": "Clean Text",
"type": "n8n-nodes-base.code",
"position": [
672,
48
],
"parameters": {
"jsCode": "// Ambil text hasil extract PDF\nlet text = $json.text || $json.data || \"\";\n\n// Pastikan string\ntext = String(text);\n\n// Hilangkan karakter aneh non-printable\ntext = text.replace(/[^\\x20-\\x7E\\n\\r]/g, \"\");\n\n// Hilangkan newline berlebihan\ntext = text.replace(/\\n\\s*\\n/g, \"\\n\");\n\n// Hilangkan spasi ganda\ntext = text.replace(/\\s+/g, \" \");\n\n// Trim\ntext = text.trim();\n\n// Batasi panjang (misal 15.000 karakter untuk safety)\nconst MAX_LENGTH = 15000;\n\nif (text.length > MAX_LENGTH) {\n text = text.substring(0, MAX_LENGTH);\n}\n\nreturn [\n {\n json: {\n cleaned_text: text\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "0e4eb4a7-25d6-49eb-9106-af82daf5209c",
"name": "Convert HTML to PDF - Exam",
"type": "n8n-nodes-htmlcsstopdf.htmlcsstopdf",
"position": [
-112,
1264
],
"parameters": {
"html_content": "={{ $json.exam_html }}"
},
"credentials": {
"htmlcsstopdfApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "8cd1950a-7eba-42e5-8903-d3becd42f2bc",
"name": "Convert HTML to PDF - Answer",
"type": "n8n-nodes-htmlcsstopdf.htmlcsstopdf",
"position": [
-112,
1072
],
"parameters": {
"html_content": "={{ $json.answer_key_html }}"
},
"credentials": {
"htmlcsstopdfApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "c8dabac7-0bfc-411b-97c7-0f6c21dcc379",
"name": "Send a text message",
"type": "n8n-nodes-base.telegram",
"position": [
128,
1264
],
"parameters": {
"text": "=Download Exam Here : {{ $json.pdf_url }}",
"chatId": "123456789",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "5216e3ac-c362-41f1-94a8-e497ae747183",
"name": "Send a text message1",
"type": "n8n-nodes-base.telegram",
"position": [
128,
1072
],
"parameters": {
"text": "=Download Answer Here : {{ $json.pdf_url }}",
"chatId": "123456789",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "ba6d1641-7c82-4cc7-a89d-b4438b6e8c53",
"name": "Length Estimation Layer",
"type": "n8n-nodes-base.code",
"position": [
880,
48
],
"parameters": {
"jsCode": "const text = $input.first().json.cleaned_text;\n\nconst estimatedTokens = Math.ceil(text.length / 4);\n\nconst MAX_TOKENS = 8000; // batas aman V1\n\nif (estimatedTokens > MAX_TOKENS) {\n return [\n {\n json: {\n error: true,\n message: \"Document too long. Please upload a shorter PDF (recommended max 15\u201320 pages).\",\n estimated_tokens: estimatedTokens\n }\n }\n ];\n}\n\nreturn [\n {\n json: {\n error : false,\n cleaned_text: text,\n estimated_tokens: estimatedTokens\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "67befabc-06a2-4518-a83a-f7c628c2f645",
"name": "Validate Token > 8.000",
"type": "n8n-nodes-base.if",
"position": [
1104,
48
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "150959fd-bbc5-4a83-a84d-45ece3122634",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.error }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "fdc5097a-d54c-4960-b95e-906a58b11405",
"name": "Format Exam Text",
"type": "n8n-nodes-base.code",
"position": [
736,
656
],
"parameters": {
"jsCode": "const mcq = $input.first().json.mcq || [];\nconst essay = $input.first().json.essay || [];\n\nlet examText = \"\";\nlet answerKey = \"\";\n\nexamText += \"<h1>Exam Paper</h1><hr>\";\n\n/* =======================\n MULTIPLE CHOICE\n======================= */\n\nif (mcq.length > 0) {\n examText += \"<h2>Multiple Choice Questions</h2>\";\n\n mcq.forEach((q, index) => {\n examText += `<p><strong>${index + 1}. ${q.question}</strong></p>`;\n examText += `<p>A. ${q.options?.A || \"\"}</p>`;\n examText += `<p>B. ${q.options?.B || \"\"}</p>`;\n examText += `<p>C. ${q.options?.C || \"\"}</p>`;\n examText += `<p>D. ${q.options?.D || \"\"}</p>`;\n });\n\n answerKey += \"<h2>Answer Key - MCQ</h2>\";\n mcq.forEach((q, index) => {\n answerKey += `<p>${index + 1}. ${q.answer}</p>`;\n });\n}\n\n/* =======================\n ESSAY\n======================= */\n\nif (essay.length > 0) {\n examText += \"<hr><h2>Essay Questions</h2>\";\n\n essay.forEach((q, index) => {\n examText += `<p><strong>${index + 1}. ${q.question}</strong></p>`;\n });\n\n answerKey += \"<hr><h2>Answer Guide - Essay</h2>\";\n essay.forEach((q, index) => {\n answerKey += `<p><strong>${index + 1}.</strong> ${q.expected_points || \"\"}</p>`;\n });\n}\n\nreturn [\n {\n json: {\n exam_html: examText,\n answer_key_html: answerKey\n }\n }\n];"
},
"typeVersion": 2
},
{
"id": "c6bb4cc0-c14e-4ccb-baa4-ab9048546cbb",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1040,
-448
],
"parameters": {
"width": 784,
"height": 2496,
"content": "# \ud83d\ude80 ExamForge AI\n### Automated PDF to Structured Exam Generator (MCQ + Essay + Answer Key)\n\nGenerate structured exams automatically from text-based PDF materials using AI.\n\nExamForge AI is a production-ready n8n workflow that transforms educational content into multiple-choice and essay questions with customizable difficulty and automatic answer key generation.\n---\n# \u2728 Features\n\n- \ud83d\udcc4 Upload PDF via Webhook\n- \u2705 File size validation (default: max 5MB)\n- \ud83e\uddf9 Automatic text cleaning\n- \ud83d\udccf Token length estimation & safety control\n- \ud83c\udfaf Customizable MCQ & Essay count\n- \ud83e\udde0 Difficulty selection (easy / medium / hard)\n- \ud83c\udf0d Language selection\n- \ud83d\udce6 Structured JSON AI output\n- \ud83d\udcdd Separate Exam PDF & Answer Key PDF\n- \ud83d\udcf2 Telegram delivery support (optional)\n- \ud83d\udd12 Parameter validation with structured error responses\n---\n# \ud83e\udde0 What This Workflow Does\n\n1. Accepts PDF upload via Webhook\n2. Validates file size\n3. Extracts and cleans text content\n4. Estimates text length to prevent token overflow\n5. Validates required parameters:\n - `mcq_count`\n - `essay_count`\n - `difficulty`\n - `language`\n6. Sends structured prompt to OpenAI\n7. Parses JSON response\n8. Formats exam and answer key separately\n9. Converts both into PDF\n10. Sends results via Telegram or Webhook response\n\n---\n\n# \u2699\ufe0f Requirements\n\n## Accounts Required\n\n- OpenAI account (API key required)\n- Telegram Bot (optional)\n- PDF Munk (API key required)\n\n## Environment\n\n- n8n (self-hosted or cloud)\n- Node version compatible with your n8n installation\n\n---\n\n# \ud83d\udd11 Credentials Setup\n\n## 1\ufe0f\u20e3 OpenAI\n\n- Add OpenAI credentials inside n8n\n- Insert your API key\n- Select preferred model (e.g., GPT-4o / GPT-4)\n\n## 2\ufe0f\u20e3 Telegram (Optional)\n\n- Create a Telegram Bot via BotFather\n- Insert Bot Token into Telegram node\n- Add your Chat ID\n\n---\n\n# \ud83d\udee0 Webhook Configuration\n\n**Method:** POST \n**Content-Type:** multipart/form-data \n\n## Required Parameters\n\n| Parameter | Type | Required | Description |\n|--------------|--------|----------|-------------|\n| file | File | Yes | PDF document |\n| mcq_count | Number | Yes | Number of multiple-choice questions |\n| essay_count | Number | Yes | Number of essay questions |\n| difficulty | String | Yes | easy / medium / hard |\n| language | String | Yes | Output language |\n\n---\n\n# \ud83d\udce5 Example Request\n\n```bash\ncurl -X POST https://your-n8n-domain/webhook/examforge \\\n -F \"file=@document.pdf\" \\\n -F \"mcq_count=20\" \\\n -F \"essay_count=5\" \\\n -F \"difficulty=medium\" \\\n -F \"language=Indonesian\""
},
"typeVersion": 1
},
{
"id": "9583f719-e279-4e99-8a1b-ba67556f3609",
"name": "Respond Valid Parameter",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
496,
272
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={\n \"status\": \"error\",\n \"message\": \"Invalid parameters\",\n \"details\":\"{{ $json.errors }}\"\n} "
},
"typeVersion": 1.4
},
{
"id": "7e297b7a-6cb0-480c-9c4b-2184b7338c9b",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
-112
],
"parameters": {
"color": 7,
"width": 1712,
"height": 528,
"content": "## Step 1 : Get File, Parsing and Validation file PDF\n"
},
"typeVersion": 1
},
{
"id": "d66457fe-5bc6-45f1-8418-bc24157d9f21",
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1296,
32
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "{\n \"status\": \"error\",\n \"message\": \"Invalid size upload\",\n \"details\":\"Document too long. Please upload a shorter PDF (recommended max 15\u201320 pages).\"\n} "
},
"typeVersion": 1.4
},
{
"id": "de840513-497b-433a-9323-6823780e16dd",
"name": "Condition Valid",
"type": "n8n-nodes-base.if",
"position": [
256,
64
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "87665a50-4cd9-4cdd-a009-5eda14975f4f",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.isValid }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "2e4212dd-5dd0-4ccc-b5d2-f72b88d0714c",
"name": "Validate Parameter & PDF",
"type": "n8n-nodes-base.code",
"position": [
48,
64
],
"parameters": {
"jsCode": "const errors = [];\n\nconst mcq = $input.first().json.body.mcq_count\nconst essay = $input.first().json.body.essay_count;\nconst difficulty = $input.first().json.body.difficulty;\nconst language = $input.first().json.body.language;\n\nlet fileSize = $input.first().binary.data.fileSize\nlet size = String(fileSize).toLowerCase();\nlet fileSizeInBytes = parseInt(\n size.includes('mb')\n ? parseFloat(size) * 1024 * 1024\n : size.includes('kb')\n ? parseFloat(size) * 1024\n : parseFloat(size)\n);\n\nif(fileSizeInBytes >=5242880){\n errors.push(\"Files cannot be more than 5MB\");\n}\n\n// Check required fields\nif (!mcq) {\n errors.push(\"mcq_count is required\");\n}\n\nif (!essay) {\n errors.push(\"essay_count is required\");\n}\n\nif (!difficulty) {\n errors.push(\"difficulty is required (easy | medium | hard)\");\n}\n\nif (!language) {\n errors.push(\"language is required\");\n}\n\n// Validate numeric\nif (mcq && isNaN(mcq)) {\n errors.push(\"mcq_count must be a number\");\n}\n\nif (essay && isNaN(essay)) {\n errors.push(\"essay_count must be a number\");\n}\n\n// Validate difficulty value\nconst allowedDifficulty = [\"easy\", \"medium\", \"hard\"];\nif (difficulty && !allowedDifficulty.includes(difficulty)) {\n errors.push(\"difficulty must be: easy, medium, or hard\");\n}\n\nif (mcq && mcq > 50) {\n errors.push(\"mcq_count maximum is 50\");\n}\n\nif (essay && essay > 50) {\n errors.push(\"essay_count maximum is 20\");\n}\n\nreturn [{\n json: {\n isValid: errors.length === 0,\n errors: errors,\n mcq_count: mcq,\n essay_count: essay,\n difficulty: difficulty,\n language: language,\n data : $input.first().binary.data\n }\n}];"
},
"typeVersion": 2
},
{
"id": "21c3c2a2-3946-4af1-a440-7949234949fc",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
432
],
"parameters": {
"color": 7,
"width": 1712,
"height": 528,
"content": "## Step 2: Generate Exam and Answer\n"
},
"typeVersion": 1
},
{
"id": "b3207d61-69a1-41ec-ad30-7bb936b858b9",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
992
],
"parameters": {
"color": 7,
"width": 1712,
"height": 528,
"content": "## Step 3: Convert HTML to PDF and Send Telegram\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "ad5f5885-5354-47d3-84e4-2231777429f5",
"connections": {
"Webhook": {
"main": [
[
{
"node": "Validate Parameter & PDF",
"type": "main",
"index": 0
}
]
]
},
"Clean Text": {
"main": [
[
{
"node": "Length Estimation Layer",
"type": "main",
"index": 0
}
]
]
},
"Parse JSON": {
"main": [
[
{
"node": "Format Exam Text",
"type": "main",
"index": 0
}
]
]
},
"Condition Valid": {
"main": [
[
{
"node": "Extract from File",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Valid Parameter",
"type": "main",
"index": 0
}
]
]
},
"Message a model": {
"main": [
[
{
"node": "Parse JSON",
"type": "main",
"index": 0
}
]
]
},
"Format Exam Text": {
"main": [
[
{
"node": "Convert HTML to PDF - Exam",
"type": "main",
"index": 0
},
{
"node": "Convert HTML to PDF - Answer",
"type": "main",
"index": 0
}
]
]
},
"Extract from File": {
"main": [
[
{
"node": "Clean Text",
"type": "main",
"index": 0
}
]
]
},
"Respond to Webhook": {
"main": [
[]
]
},
"Validate Token > 8.000": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
],
[
{
"node": "Message a model",
"type": "main",
"index": 0
}
]
]
},
"Length Estimation Layer": {
"main": [
[
{
"node": "Validate Token > 8.000",
"type": "main",
"index": 0
}
]
]
},
"Validate Parameter & PDF": {
"main": [
[
{
"node": "Condition Valid",
"type": "main",
"index": 0
}
]
]
},
"Convert HTML to PDF - Exam": {
"main": [
[
{
"node": "Send a text message",
"type": "main",
"index": 0
}
]
]
},
"Convert HTML to PDF - Answer": {
"main": [
[
{
"node": "Send a text message1",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
htmlcsstopdfApiopenAiApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Generate structured exams automatically from text-based PDF materials using AI.
Source: https://n8n.io/workflows/13599/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Listens for completed Fireflies transcripts, qualifies whether a proposal is needed using OpenAI, drafts structured proposal content, populates a Google Doc template, converts to PDF, and sends it to
Monitor and manage Docker containers from Telegram with AI log analysis
Bubu Telegram Companion. Uses httpRequest, openAi, errorTrigger, telegram. Webhook trigger; 31 nodes.
Bot ROVEEb. Uses openAi, dataTable, telegram, spreadsheetFile. Webhook trigger; 31 nodes.
This workflow turns a Telegram bot into an AI-powered lyrics assistant. Users send a command plus a lyrics URL, and the flow downloads, cleans, and analyzes the text, then replies on Telegram with tra