{
  "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
          }
        ]
      ]
    }
  }
}