AutomationFlowsData & Sheets › Offline – Build Question Bank From Lesson (lesson Bundle → Supabase) 12/2

Offline – Build Question Bank From Lesson (lesson Bundle → Supabase) 12/2

Offline – Build Question Bank from Lesson (Lesson Bundle → Supabase) 12/2. Uses httpRequest, supabase. Event-driven trigger; 15 nodes.

Event trigger★★★★☆ complexity15 nodesHTTP RequestSupabase
Data & Sheets Trigger: Event Nodes: 15 Complexity: ★★★★☆ Added:

This workflow follows the HTTP Request → Supabase 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 →

Download .json
{
  "name": "Offline \u2013 Build Question Bank from Lesson (Lesson Bundle \u2192 Supabase) 12/2",
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        16,
        320
      ],
      "id": "94765859-be5a-4c59-84cf-701c2f8110a5",
      "name": "When clicking \u2018Execute workflow\u2019"
    },
    {
      "parameters": {
        "url": "https://raw.githubusercontent.com/norahkerendian/dsc180a-q1/refs/heads/main/data_scraping/inferential_lessons.json?token=GHSAT0AAAAAADL5WWDGDRJBCP2OXVJ7YA2Q2JPTETQ",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        240,
        320
      ],
      "id": "e2ac2e11-822b-493a-81ba-6a4ad92a4434",
      "name": "HTTP Request (unused for now)"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "lesson_slug",
              "name": "lesson_slug",
              "value": "={{ $json.slug }}",
              "type": "string"
            },
            {
              "id": "lesson_title",
              "name": "lesson_title",
              "value": "={{ $json.title }}",
              "type": "string"
            },
            {
              "id": "topic",
              "name": "topic",
              "value": "={{ $json.topic || $json.title }}",
              "type": "string"
            },
            {
              "id": "level",
              "name": "level",
              "value": "={{ $json.level }}",
              "type": "number"
            },
            {
              "id": "lesson_text",
              "name": "lesson_text",
              "value": "=={{ $json.text_chunk }}",
              "type": "string"
            },
            {
              "id": "0c3999ed-6dbf-4ed5-b2eb-e865e9587c59",
              "name": "chunk_id",
              "value": "={{ $json.chunk_index }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1072,
        320
      ],
      "id": "04d3df20-23b9-48fd-99b5-f5fa5112d908",
      "name": "Set \u2013 Lesson Metadata"
    },
    {
      "parameters": {
        "functionCode": "// DeepSeek returns JSON inside choices[0].message.content.\n\nconst raw =\n  $json.choices &&\n  Array.isArray($json.choices) &&\n  $json.choices[0] &&\n  $json.choices[0].message &&\n  typeof $json.choices[0].message.content === 'string'\n    ? $json.choices[0].message.content.trim()\n    : '';\n\nif (!raw) {\n  throw new Error('LLM response is empty or missing choices[0].message.content');\n}\n\n// Clean markdown code fences and DeepSeek <think> tags\nconst cleaned = raw\n  // remove ```json and ``` fences\n  .replace(/```json/gi, '')\n  .replace(/```/g, '')\n  // remove DeepSeek r1 thinking blocks if present\n  .replace(/<think>[\\\\s\\\\S]*?<\\\\/think>/gi, '')\n  .trim();\n\nlet bundle;\n\ntry {\n  bundle = JSON.parse(cleaned);\n} catch (err) {\n  throw new Error(\n    'Failed to parse LLM JSON: ' +\n      err.message +\n      '\\nRaw content (first 500 chars):\\n' +\n      cleaned.slice(0, 500)\n  );\n}\n\n// Expecting shape:\n// {\n//   \"lesson\": { \"slug\": \"...\", \"title\": \"...\", \"topic\": \"...\", \"level\": 0 },\n//   \"questions\": [ ... ]\n// }\nconst lesson = bundle.lesson || {};\nconst questions = Array.isArray(bundle.questions) ? bundle.questions : [];\n\nconst metaNode = $node['Set \u2013 Lesson Metadata'].json || {};\n\nreturn questions.map((q, index) => ({\n  json: {\n    // lesson metadata (fallback-safe)\n    lesson_slug: lesson.slug || metaNode.lesson_slug,\n    lesson_title: lesson.title || metaNode.lesson_title,\n    topic: lesson.topic || metaNode.topic,\n    level:\n      typeof lesson.level === 'number'\n        ? lesson.level\n        : metaNode.level,\n\n    // question info\n    question: q.question,\n    answer: q.answer,\n    difficulty: typeof q.difficulty === 'number' ? q.difficulty : 1,\n    category: q.category || 'unspecified',\n    question_type: q.question_type || 'short_answer',\n    source: 'baseline',\n    index,\n  }\n}));\n"
      },
      "type": "n8n-nodes-base.function",
      "typeVersion": 1,
      "position": [
        2416,
        320
      ],
      "id": "80c16f3d-ee34-44da-bdb2-be2494e5b9b5",
      "name": "Function \u2013 Flatten Lesson Bundle"
    },
    {
      "parameters": {
        "operation": "insert"
      },
      "type": "n8n-nodes-base.supabase",
      "typeVersion": 1,
      "position": [
        2688,
        320
      ],
      "id": "5625e78a-f95f-4349-8782-a88cca0b4bf2",
      "name": "Supabase \u2013 Insert Questions"
    },
    {
      "parameters": {
        "content": "## Notes\n1. This workflow builds a question bank from a single LESSON_TEXT using DeepSeek.\n2. The Set \u2013 Lesson Metadata node is where you paste your synthetic lesson for now.\n3. Supabase \u2013 Insert Questions needs column mappings configured in the n8n UI.\n4. Later, we can swap the Set node to pull lessons from Supabase instead of hardcoding.",
        "height": 240,
        "width": 560
      },
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        0
      ],
      "id": "f2c6d2b4-9eef-482e-a0e3-1160cb1eed5f",
      "name": "Sticky Note \u2013 Workflow 1 Overview"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dee42752-1db6-45d5-b514-2c829145a877",
              "name": "parsedData",
              "value": "={{ JSON.parse($json.data) }}",
              "type": "array"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        448,
        320
      ],
      "id": "75158c1e-c889-4683-ae34-dd6f85998aea",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "jsCode": "// const inputItems = $input.all();\n// const chunkSize = 800;\n\n// let out = [];\n\n// for (const item of inputItems) {\n//   const lesson = item.json;\n//   const text = lesson.content_md || \"\";\n\n//   for (let i = 0; i < text.length; i += chunkSize) {\n//     out.push({\n//       json: {\n//         ...lesson,\n//         text_chunk: text.slice(i, i + chunkSize),\n//         chunk_index: Math.floor(i / chunkSize),\n//       }\n//     });\n//   }\n// }\n\n// return out;\n\nconst chunkSize = 900;\n\nfunction clean(str) {\n  return str\n    .replace(/\\r?\\n+/g, \" \")        // remove line breaks\n    .replace(/\\s+/g, \" \")           // collapse whitespace\n    .replace(/\\[.*?\\]\\(.*?\\)/g, \"\") // remove markdown links\n    .replace(/[#*_`>~=-]/g, \"\")     // remove symbols\n    .trim();\n}\n\nconst inputItems = $input.all();\nlet out = [];\n\nfor (const item of inputItems) {\n  const lesson = item.json;\n  const text = clean(lesson.content_md || \"\");\n\n  for (let i = 0; i < text.length; i += chunkSize) {\n    out.push({\n      json: {\n        ...lesson,\n        text_chunk: text.slice(i, i + chunkSize),\n        chunk_index: Math.floor(i / chunkSize),\n      }\n    });\n  }\n}\n\nreturn out;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        864,
        320
      ],
      "id": "f4b742dc-638f-4dd9-a773-55a1358a08cb",
      "name": "chunking"
    },
    {
      "parameters": {
        "jsCode": "// parsedData should be an array of lessons from your JSON\nconst lessons = $json.parsedData;\n\n// Optional filter if you want to test only some levels first:\n// const filtered = lessons.filter(l => l.level <= 1);\n// const source = filtered;\n\nconst source = lessons;\n\nreturn source.map(item => ({\n  json: item\n}));"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        656,
        320
      ],
      "id": "6e2f094c-0266-46ba-b5d5-f7f48f9ae762",
      "name": "mapping"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11434/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"model\": \"deepseek-r1:1.5b\",\n  \"messages\": [\n    {\n      \"role\": \"system\",\n      \"content\": \"You generate high-quality beginner-friendly quiz questions using only the information in the provided answers. Your job is to write clear, factual questions whose answers exactly match the provided text. Return JSON only.\"\n    },\n    {\n      \"role\": \"user\",\n      \"content\": \"Below is a list of factual answers extracted from a lesson. Write a corresponding question for each answer. Rules: Use only the information in the answers. Do not introduce new facts. Keep questions simple and direct. Return JSON only in this exact format: { \\\"qa_pairs\\\": [ { \\\"question\\\": \\\"...\\\", \\\"answer\\\": \\\"...\\\" }, { \\\"question\\\": \\\"...\\\", \\\"answer\\\": \\\"...\\\" } ] }. Answers: {{ $json.answers }}\"\n    }\n  ],\n  \"temperature\": 0.2\n}\n",
        "options": {}
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2224,
        320
      ],
      "id": "23aa715e-cebd-417a-a219-ae888d0d21e0",
      "name": "generate questions"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11434/v1/chat/completions",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\n  \"model\":\"llama3.2:1b\",\n  \"messages\":[\n    {\"role\":\"system\",\"content\":\"Summarize the lesson in 1-2 short, factual sentences for beginners.\"},\n    {\"role\":\"user\",\"content\":\"Text: {{ $json.safe_text }}\"}\n  ],\n  \"max_tokens\":120,\n  \"temperature\":0.2\n}\n",
        "options": {
          "timeout": 240000
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1872,
        320
      ],
      "id": "ee8edea5-d9d8-447f-8df1-d4901af29f55",
      "name": "generate answers"
    },
    {
      "parameters": {
        "jsCode": "const raw = $json.choices?.[0]?.message?.content || \"\";\nconst cleaned = raw\n  .replace(/```json/gi, \"\")\n  .replace(/```/g, \"\")\n  .replace(/<think>[\\s\\S]*?<\\/think>/gi, \"\")\n  .trim();\n\nreturn [\n  {\n    json: {\n      summary: cleaned\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2048,
        320
      ],
      "id": "3317d2f7-1a81-41bf-8b9f-bc96a1fdf4a6",
      "name": "extract summary"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        1280,
        320
      ],
      "id": "4be667af-592d-4609-b713-b8a6a78ac4d5",
      "name": "Loop Over Items"
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.noOp",
      "name": "Replace Me",
      "typeVersion": 1,
      "position": [
        1696,
        384
      ],
      "id": "0c36188a-8a2e-4b83-a018-2de3645f07dc"
    },
    {
      "parameters": {
        "jsCode": "// Take raw chunk\nlet text = ($json.lesson_text || \"\").toString();\n\n// Clean & shrink aggressively for llama3.2:1b stability\ntext = text\n  .replace(/\\r?\\n+/g, \" \")\n  .replace(/\\s+/g, \" \")\n  .replace(/\\[.*?\\]\\(.*?\\)/g, \"\")\n  .replace(/[#*_`>~=-]/g, \"\")\n  .trim()\n  .slice(0, 600);   // HARD limit\n\nreturn [\n  {\n    json: {\n      ...$json,\n      safe_text: text\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1488,
        224
      ],
      "id": "9f1ab716-784e-4941-99b0-6ae1e5ea75df",
      "name": "Prepare Lesson Text"
    }
  ],
  "connections": {
    "When clicking \u2018Execute workflow\u2019": {
      "main": [
        [
          {
            "node": "HTTP Request (unused for now)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request (unused for now)": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set \u2013 Lesson Metadata": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Function \u2013 Flatten Lesson Bundle": {
      "main": [
        [
          {
            "node": "Supabase \u2013 Insert Questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "mapping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "chunking": {
      "main": [
        [
          {
            "node": "Set \u2013 Lesson Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "mapping": {
      "main": [
        [
          {
            "node": "chunking",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate questions": {
      "main": [
        [
          {
            "node": "Function \u2013 Flatten Lesson Bundle",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "generate answers": {
      "main": [
        [
          {
            "node": "extract summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "extract summary": {
      "main": [
        [
          {
            "node": "generate questions",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [
          {
            "node": "Prepare Lesson Text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Replace Me",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Replace Me": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Lesson Text": {
      "main": [
        [
          {
            "node": "generate answers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "5ca0d598-c390-4b54-9b72-d118de2eca1b",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "id": "mwe4eSWKC9ZIEhX9",
  "tags": []
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Offline – Build Question Bank from Lesson (Lesson Bundle → Supabase) 12/2. Uses httpRequest, supabase. Event-driven trigger; 15 nodes.

Source: https://github.com/norahkerendian/dsc180-capstone-project-datalingo/blob/90b85da8682dcccdd3d1eecf2af4799226993d91/workflows/with_loop.json — original creator credit. Request a take-down →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

This enables webhooks for nearly realtime updates (every 5 seconds) from Notion Databases.

Notion, Supabase, Execute Workflow Trigger +1
Data & Sheets

dummy_client - Shopify abandoned carts. Uses httpRequest, shopifyTrigger, whatsApp, supabase. Event-driven trigger; 25 nodes.

HTTP Request, Shopify Trigger, WhatsApp +2
Data & Sheets

This workflow automatically collects historical price data from Polymarket Up/Down markets and stores it in Supabase, creating a structured and query-ready dataset for analysis. By continuously fetchi

Form Trigger, HTTP Request, Data Table +1
Data & Sheets

Webhook. Uses manualTrigger, stickyNote, httpRequest, supabase. Event-driven trigger; 21 nodes.

HTTP Request, Supabase
Data & Sheets

You provide a list of prompts and a system instruction, the workflow batches them into a single OpenAI Batch API request. The batch job is tracked in a Supabase openai_batches table. A cron job polls

HTTP Request, Supabase, Postgres