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
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
LU. Uses telegram, openAi, httpRequest. Webhook trigger; 28 nodes.
Transform customer feedback into actionable insights automatically with AI analysis, professional PDF reports, personalized emails, and real-time team notifications. Overview Features Demo Prerequisit