AutomationFlowsAI & RAG › Generate Rag-based Study Flashcards From Notes with Openai and Slack

Generate Rag-based Study Flashcards From Notes with Openai and Slack

ByOneclick AI Squad @oneclick-ai on n8n.io

This workflow turns submitted study notes into a reviewed flashcard deck by chunking the text, retrieving the most relevant passages with OpenAI embeddings, generating grounded Q&A cards with an OpenAI chat model, and sending the approved deck to a Slack channel. Receives notes…

Webhook trigger★★★★☆ complexityAI-powered20 nodesHTTP RequestOpenAI ChatChain LlmSlack
AI & RAG Trigger: Webhook Nodes: 20 Complexity: ★★★★☆ AI nodes: yes Added:

This workflow corresponds to n8n.io template #18210 — we link there as the canonical source.

This workflow follows the Chainllm → HTTP Request 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
{
  "id": "4F8QHfCLnA8xrj3a",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Flashcard Generator - RAG (Notes as Source) + LLM",
  "tags": [],
  "nodes": [
    {
      "id": "5220ea01-c462-4b25-8663-2a29d59a129b",
      "name": "Sticky Note - Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5024,
        -368
      ],
      "parameters": {
        "width": 1040,
        "height": 1020,
        "content": "## Flashcard Generator (RAG over your notes + LLM)\n\nTurns raw study notes into a reviewed, spaced-repetition-ready flashcard deck. Notes are chunked and embedded, the most relevant passages are retrieved for the topic you ask about, an LLM drafts flashcards strictly grounded in that retrieved context, and the deck gets a human review pass before export.\n\n### How it works\n1. Notes are submitted (webhook) or the Manual Trigger is used for testing\n2. Set - Config centralizes chunking size/overlap, the embeddings model, retrieval top-K/threshold, target flashcard count and the notify channel\n3. JS - Chunk Notes & Prepare Embedding Requests recursively splits the notes into paragraph/sentence-aware chunks with overlap and heading metadata (advanced, hand-rolled RAG ingestion \u2014 no external chunking service needed)\n4. The flow pauses (Wait) for you to say what topic/section you want cards on\n5. HTTP Request - Get Embeddings sends ONE batched call: your topic query plus every chunk, in a single request \u2014 this is the workflow's one external API call\n6. JS - Vector Similarity Search & Context Assembly computes cosine similarity in-flow, then runs a Maximal-Marginal-Relevance pass so the retrieved context is relevant AND non-redundant\n7. AI - Generate Flashcards from Context drafts question/answer pairs strictly grounded in the retrieved passages\n8. JS - Parse, Validate, Dedupe & Assign SM-2 Scheduling validates the LLM's JSON, removes near-duplicate cards (Jaccard similarity), and initializes SM-2 spaced-repetition parameters (easiness factor, interval, next review date) for each card\n9. The flow pauses twice more \u2014 once to review/edit the drafted cards, once to confirm export \u2014 before the deck is delivered\n\n### Requirements\n- An embeddings API (e.g. OpenAI text-embedding-3-small)\n- An LLM for generation (e.g. GPT-4.1-mini or above)\n- Slack app/bot token with chat:write scope (or swap for Anki/Notion export)\n\n### How to customize\n- Tune chunkSizeChars/chunkOverlapChars for denser or sparser notes\n- Raise retrievalTopK for broader coverage, or similarityThreshold to be stricter about relevance\n- Swap the SM-2 initializer in the third code node for a different spaced-repetition algorithm (e.g. FSRS)\n- Point the final delivery node at Anki's AnkiConnect, Notion, or a CSV export instead of Slack"
      },
      "typeVersion": 1
    },
    {
      "id": "f526624c-5e13-40fa-9ef6-4099f3be1dd4",
      "name": "Sticky Note - Ingest & Chunk",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6240,
        -336
      ],
      "parameters": {
        "color": 6,
        "width": 900,
        "height": 900,
        "content": "## 1. Trigger, Config & Advanced Chunking\n\nSet - Config centralizes every knob: chunk size/overlap, embeddings model, retrieval top-K/threshold, flashcard count, notify channel.\n\nJS - Chunk Notes & Prepare Embedding Requests is the first advanced code node: it splits notes into paragraphs, then sentences, then greedily re-merges sentences into chunks near the target size, carrying a trailing overlap window forward into the next chunk so context isn't lost at chunk boundaries, and tagging each chunk with any markdown heading it falls under.\n\nWait - For Topic/Scope Selection then pauses for you to specify what to make cards about."
      },
      "typeVersion": 1
    },
    {
      "id": "db409f7c-331b-4291-964c-05c4329827d1",
      "name": "Sticky Note - Retrieve & Generate",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        7200,
        -352
      ],
      "parameters": {
        "color": 6,
        "width": 1360,
        "height": 900,
        "content": "## 2. Batched Embeddings, MMR Retrieval & Generation\n\nHTTP Request - Get Embeddings sends the topic query and every chunk's text together in a single batched request \u2014 the workflow's only external API call, used for both the query and the index.\n\nJS - Vector Similarity Search & Context Assembly is the second advanced code node: it computes cosine similarity by hand between the query vector and every chunk vector, filters by a similarity threshold, then runs a Maximal Marginal Relevance loop (word-overlap based redundancy penalty) to pick a top-K set of chunks that's relevant without being repetitive.\n\nAI - Generate Flashcards from Context then drafts question/answer pairs strictly grounded in that assembled context."
      },
      "typeVersion": 1
    },
    {
      "id": "eb986f1d-95d8-4cda-ba97-d97b69d613ac",
      "name": "Sticky Note - Validate, Review & Export",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        8624,
        -400
      ],
      "parameters": {
        "color": 6,
        "width": 1560,
        "height": 900,
        "content": "## 3. Validate/Schedule, Human Review & Export\n\nJS - Parse, Validate, Dedupe & Assign SM-2 Scheduling is the third advanced code node: it safely parses the LLM's JSON, drops any card missing a question/answer, removes near-duplicate cards using Jaccard similarity over word sets, and initializes SM-2 spaced-repetition state (easiness factor 2.5, 0-day interval, 0 repetitions, next review = today) on every surviving card.\n\nWait - For Flashcard Review & Edits pauses for a human pass over the drafted deck. IF - Review Approved gates delivery; approved decks pause once more at Wait - For Export Confirmation, then IF - Export Approved sends the deck via Slack - Send Flashcard Deck (swap for Anki/Notion). Declined decks land on Set - Discarded/Cancelled."
      },
      "typeVersion": 1
    },
    {
      "id": "92ee700f-0bd5-4f4d-989e-b1f988d1dcc9",
      "name": "Webhook - Notes Submitted",
      "type": "n8n-nodes-base.webhook",
      "position": [
        6336,
        32
      ],
      "parameters": {
        "path": "flashcard-notes-submit",
        "options": {},
        "httpMethod": "POST"
      },
      "typeVersion": 2
    },
    {
      "id": "92013bb5-c2ae-46f9-8c1f-2722fd38c87a",
      "name": "Manual Trigger - Test Run",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        6336,
        224
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "b7dfb37f-82dc-4466-8396-57b6c1a503f9",
      "name": "Set - Config",
      "type": "n8n-nodes-base.set",
      "position": [
        6560,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "embeddingApiUrl",
              "type": "string",
              "value": "https://api.openai.com/v1/embeddings"
            },
            {
              "name": "embeddingModel",
              "type": "string",
              "value": "text-embedding-3-small"
            },
            {
              "name": "chunkSizeChars",
              "type": "number",
              "value": 800
            },
            {
              "name": "chunkOverlapChars",
              "type": "number",
              "value": 120
            },
            {
              "name": "retrievalTopK",
              "type": "number",
              "value": 6
            },
            {
              "name": "similarityThreshold",
              "type": "number",
              "value": 0.15
            },
            {
              "name": "flashcardCount",
              "type": "number",
              "value": 10
            },
            {
              "name": "defaultTopic",
              "type": "string",
              "value": "General review of the whole document"
            },
            {
              "name": "notifyChannel",
              "type": "string",
              "value": "#flashcards"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "964b5757-3c6e-49a7-8641-7ef38484a832",
      "name": "JS - Chunk Notes & Prepare Embedding Requests",
      "type": "n8n-nodes-base.code",
      "position": [
        6784,
        112
      ],
      "parameters": {
        "jsCode": "// Advanced RAG ingestion: paragraph/sentence-aware recursive chunking with\n// overlap carried across boundaries and markdown-heading metadata per chunk.\nconst config = $('Set - Config').first().json;\nconst raw = String($input.first().json.notesText || '');\n\nconst targetChunkSize = config.chunkSizeChars || 800;\nconst overlapChars = config.chunkOverlapChars || 120;\n\nfunction splitIntoParagraphs(text) {\n  return text.split(/\\n{2,}/).map(p => p.trim()).filter(Boolean);\n}\n\nfunction splitIntoSentences(text) {\n  const matches = text.match(/[^.!?]+[.!?]+(\\s+|$)|[^.!?]+$/g);\n  return (matches || [text]).map(s => s.trim()).filter(Boolean);\n}\n\nfunction detectHeading(paragraph) {\n  const m = paragraph.match(/^(#{1,3})\\s+(.*)$/m);\n  return m ? m[2].trim() : null;\n}\n\nconst paragraphs = splitIntoParagraphs(raw);\nconst chunks = [];\nlet buffer = '';\nlet bufferHeading = null;\nlet carryOverlap = '';\n\nfunction pushChunk(text, heading) {\n  const trimmed = text.trim();\n  if (!trimmed) return;\n  chunks.push({\n    chunkId: 'chunk_' + (chunks.length + 1).toString().padStart(3, '0'),\n    index: chunks.length,\n    heading: heading || null,\n    text: trimmed,\n    charCount: trimmed.length,\n    tokenEstimate: Math.ceil(trimmed.length / 4)\n  });\n}\n\nfor (const para of paragraphs) {\n  const heading = detectHeading(para);\n  if (heading) bufferHeading = heading;\n\n  const sentences = splitIntoSentences(para);\n  for (const sentence of sentences) {\n    const candidate = (buffer + ' ' + sentence).trim();\n    if (candidate.length > targetChunkSize && buffer.length > 0) {\n      pushChunk(carryOverlap + ' ' + buffer, bufferHeading);\n      carryOverlap = buffer.slice(Math.max(0, buffer.length - overlapChars));\n      buffer = sentence;\n    } else {\n      buffer = candidate;\n    }\n  }\n}\nif (buffer.trim()) pushChunk(carryOverlap + ' ' + buffer, bufferHeading);\n\nif (chunks.length === 0) {\n  chunks.push({\n    chunkId: 'chunk_001',\n    index: 0,\n    heading: null,\n    text: raw.trim() || '(no notes provided)',\n    charCount: raw.length,\n    tokenEstimate: Math.ceil(raw.length / 4)\n  });\n}\n\nreturn [{\n  json: {\n    ...config,\n    notesText: raw,\n    chunks,\n    totalChunks: chunks.length,\n    totalTokenEstimate: chunks.reduce((sum, c) => sum + c.tokenEstimate, 0)\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "a5926892-8934-470d-8ce6-910ce6ba525f",
      "name": "Wait - For Topic/Scope Selection",
      "type": "n8n-nodes-base.wait",
      "position": [
        7008,
        112
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "335af409-5875-46d0-beb8-888666725c25",
      "name": "HTTP Request - Get Embeddings",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        7232,
        112
      ],
      "parameters": {
        "url": "={{ $('JS - Chunk Notes & Prepare Embedding Requests').item.json.embeddingApiUrl }}",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.stringify({ model: $('JS - Chunk Notes & Prepare Embedding Requests').item.json.embeddingModel, input: [ $json.topic || $('JS - Chunk Notes & Prepare Embedding Requests').item.json.defaultTopic, ...$('JS - Chunk Notes & Prepare Embedding Requests').item.json.chunks.map(c => c.text) ] }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi"
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "50f1d8c5-9ff0-4548-a347-4179228d8c92",
      "name": "JS - Vector Similarity Search & Context Assembly",
      "type": "n8n-nodes-base.code",
      "position": [
        7456,
        112
      ],
      "parameters": {
        "jsCode": "// Advanced retrieval: hand-rolled cosine similarity ranking followed by a\n// Maximal Marginal Relevance pass so retrieved chunks are relevant AND diverse.\nconst embResponse = $input.first().json;\nconst ctx = $('JS - Chunk Notes & Prepare Embedding Requests').first().json;\nconst topicCtx = $('Wait - For Topic/Scope Selection').first().json;\n\nconst vectors = (embResponse.data || []).map(d => d.embedding).filter(Boolean);\n\nif (vectors.length < 2) {\n  return [{\n    json: {\n      ...ctx,\n      topic: topicCtx.topic || ctx.defaultTopic,\n      retrievedChunks: [],\n      retrievedContext: '',\n      retrievedCount: 0,\n      retrievalError: 'insufficient_embeddings'\n    }\n  }];\n}\n\nconst queryVector = vectors[0];\nconst chunkVectors = vectors.slice(1);\n\nfunction dot(a, b) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i] * b[i]; return s; }\nfunction norm(a) { return Math.sqrt(dot(a, a)); }\nfunction cosineSim(a, b) { const n = norm(a) * norm(b); return n === 0 ? 0 : dot(a, b) / n; }\n\nconst scored = ctx.chunks.map((chunk, i) => ({\n  ...chunk,\n  similarity: cosineSim(queryVector, chunkVectors[i] || [])\n})).sort((a, b) => b.similarity - a.similarity);\n\nconst topK = ctx.retrievalTopK || 6;\nconst simThreshold = ctx.similarityThreshold ?? 0.15;\nconst lambda = 0.7; // relevance vs diversity trade-off for MMR\n\nfunction wordOverlapScore(a, b) {\n  const wa = new Set(a.text.toLowerCase().split(/\\W+/).filter(Boolean));\n  const wb = new Set(b.text.toLowerCase().split(/\\W+/).filter(Boolean));\n  if (wa.size === 0 || wb.size === 0) return 0;\n  const intersection = [...wa].filter(w => wb.has(w)).length;\n  const union = new Set([...wa, ...wb]).size;\n  return union === 0 ? 0 : intersection / union;\n}\n\nconst pool = scored.filter(c => c.similarity >= simThreshold);\nconst selected = [];\n\nwhile (selected.length < topK && pool.length > 0) {\n  let bestIdx = 0;\n  let bestScore = -Infinity;\n  for (let i = 0; i < pool.length; i++) {\n    const relevance = pool[i].similarity;\n    const maxRedundancy = selected.length\n      ? Math.max(...selected.map(s => wordOverlapScore(s, pool[i])))\n      : 0;\n    const mmrScore = lambda * relevance - (1 - lambda) * maxRedundancy;\n    if (mmrScore > bestScore) { bestScore = mmrScore; bestIdx = i; }\n  }\n  selected.push(pool.splice(bestIdx, 1)[0]);\n}\n\nconst retrievedContext = selected\n  .map((c, i) => '[Source ' + (i + 1) + (c.heading ? ' - ' + c.heading : '') + ']\\n' + c.text)\n  .join('\\n\\n');\n\nreturn [{\n  json: {\n    ...ctx,\n    topic: topicCtx.topic || ctx.defaultTopic,\n    retrievedChunks: selected,\n    retrievedContext,\n    retrievedCount: selected.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c879cc0c-7ffa-41a9-9c92-6850c3d19c61",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        7680,
        304
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4.1-mini"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "2a58efe7-268c-411e-a88f-9558a82256dc",
      "name": "AI - Generate Flashcards from Context",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        7632,
        32
      ],
      "parameters": {
        "text": "=You are a study-flashcard writer. Using ONLY the retrieved context below (do not invent facts not present in it), write {{ $('JS - Vector Similarity Search & Context Assembly').item.json.flashcardCount }} high-quality flashcards on the topic: \"{{ $('JS - Vector Similarity Search & Context Assembly').item.json.topic }}\".\n\nRetrieved context:\n{{ $('JS - Vector Similarity Search & Context Assembly').item.json.retrievedContext }}\n\nEach card should test one clear, specific fact or concept (avoid vague or compound questions). Vary difficulty across the deck. Return ONLY a JSON array (no markdown fences, no commentary), each element an object with keys: question, answer, tags (array of short keyword strings), difficulty ('easy'|'medium'|'hard'), sourceHeading (the heading of the source passage it came from, or null).",
        "promptType": "define"
      },
      "typeVersion": 1.5
    },
    {
      "id": "e72ff537-c3bd-4385-9552-da7f2b685aad",
      "name": "JS - Parse, Validate, Dedupe & Assign SM-2 Scheduling",
      "type": "n8n-nodes-base.code",
      "position": [
        7920,
        32
      ],
      "parameters": {
        "jsCode": "// Advanced post-processing: safe JSON parse, schema validation, Jaccard-similarity\n// dedup, and SM-2 spaced-repetition state initialization for every surviving card.\nconst item = $input.first().json;\nconst raw = item.text || item.output || item.response || '[]';\nconst ctx = $('JS - Vector Similarity Search & Context Assembly').first().json;\n\nlet cards = [];\ntry {\n  const cleaned = String(raw).replace(/```json|```/g, '').trim();\n  const parsed = JSON.parse(cleaned);\n  cards = Array.isArray(parsed) ? parsed : (parsed.flashcards || []);\n} catch (e) {\n  cards = [];\n}\n\n// Schema validation: require non-empty question and answer strings\nconst valid = cards.filter(c => c\n  && typeof c.question === 'string' && c.question.trim()\n  && typeof c.answer === 'string' && c.answer.trim());\n\n// Dedupe near-duplicate questions using Jaccard similarity over word sets\nfunction jaccard(a, b) {\n  const wa = new Set(a.toLowerCase().split(/\\W+/).filter(Boolean));\n  const wb = new Set(b.toLowerCase().split(/\\W+/).filter(Boolean));\n  if (wa.size === 0 || wb.size === 0) return 0;\n  const intersection = [...wa].filter(w => wb.has(w)).length;\n  const union = new Set([...wa, ...wb]).size;\n  return union === 0 ? 0 : intersection / union;\n}\n\nconst DUPLICATE_THRESHOLD = 0.75;\nconst deduped = [];\nfor (const card of valid) {\n  const isDuplicate = deduped.some(existing => jaccard(existing.question, card.question) > DUPLICATE_THRESHOLD);\n  if (!isDuplicate) deduped.push(card);\n}\n\n// Initialize SM-2 spaced-repetition state for each surviving card\nconst today = new Date().toISOString().slice(0, 10);\nconst finalCards = deduped.map((c, i) => ({\n  cardId: 'card_' + (i + 1).toString().padStart(3, '0'),\n  question: c.question.trim(),\n  answer: c.answer.trim(),\n  tags: Array.isArray(c.tags) ? c.tags : [],\n  difficulty: ['easy', 'medium', 'hard'].includes(c.difficulty) ? c.difficulty : 'medium',\n  sourceHeading: c.sourceHeading || null,\n  sm2: {\n    easinessFactor: 2.5,\n    intervalDays: 0,\n    repetitions: 0,\n    nextReviewDate: today\n  }\n}));\n\nreturn [{\n  json: {\n    ...ctx,\n    flashcards: finalCards,\n    flashcardCount: finalCards.length,\n    duplicatesRemoved: valid.length - deduped.length,\n    invalidCardsDropped: cards.length - valid.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ffb7c585-915e-4727-88be-4615e3f46827",
      "name": "Wait - For Flashcard Review & Edits",
      "type": "n8n-nodes-base.wait",
      "position": [
        8144,
        32
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "dee5a5e5-cf50-451c-8e05-c1a6a6699f12",
      "name": "IF - Review Approved",
      "type": "n8n-nodes-base.if",
      "position": [
        8384,
        32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.reviewApproved === true || $json.reviewApproved === 'true' }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6beec217-193a-4b48-b280-a1dcb8f906af",
      "name": "Wait - For Export Confirmation",
      "type": "n8n-nodes-base.wait",
      "position": [
        8672,
        -48
      ],
      "parameters": {},
      "typeVersion": 1.1
    },
    {
      "id": "a5cd91f6-e4fb-43b6-9c63-4125408cae7b",
      "name": "IF - Export Approved",
      "type": "n8n-nodes-base.if",
      "position": [
        8864,
        -48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 1,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "operator": {
                "type": "boolean",
                "operation": "true"
              },
              "leftValue": "={{ $json.exportApproved === true || $json.exportApproved === 'true' }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c24d37f5-0659-4410-b051-7796fa6f6064",
      "name": "Slack - Send Flashcard Deck",
      "type": "n8n-nodes-base.slack",
      "position": [
        9104,
        -128
      ],
      "parameters": {
        "text": "=\ud83d\uddc2\ufe0f *Flashcard Deck: {{ $json.topic }}*\n\n*Cards:* {{ $json.flashcardCount }} (deduped {{ $json.duplicatesRemoved }}, dropped {{ $json.invalidCardsDropped }} invalid)\n\n{{ $json.flashcards.map((c, i) => (i+1) + '. Q: ' + c.question + '\\n   A: ' + c.answer + '  [' + c.difficulty + ']').join('\\n\\n') }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.notifyChannel }}"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3,
      "continueOnFail": true
    },
    {
      "id": "70973e2d-e9db-4671-b177-90da4eae6faa",
      "name": "Set - Discarded/Cancelled",
      "type": "n8n-nodes-base.set",
      "position": [
        9104,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "name": "finalStatus",
              "type": "string",
              "value": "discarded"
            }
          ]
        }
      },
      "typeVersion": 3.4
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "f6866920-5e10-4311-ace2-3785b183e051",
  "nodeGroups": [],
  "connections": {
    "Set - Config": {
      "main": [
        [
          {
            "node": "JS - Chunk Notes & Prepare Embedding Requests",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI - Generate Flashcards from Context",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "IF - Export Approved": {
      "main": [
        [
          {
            "node": "Slack - Send Flashcard Deck",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set - Discarded/Cancelled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF - Review Approved": {
      "main": [
        [
          {
            "node": "Wait - For Export Confirmation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set - Discarded/Cancelled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger - Test Run": {
      "main": [
        [
          {
            "node": "Set - Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook - Notes Submitted": {
      "main": [
        [
          {
            "node": "Set - Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request - Get Embeddings": {
      "main": [
        [
          {
            "node": "JS - Vector Similarity Search & Context Assembly",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - For Export Confirmation": {
      "main": [
        [
          {
            "node": "IF - Export Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - For Topic/Scope Selection": {
      "main": [
        [
          {
            "node": "HTTP Request - Get Embeddings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait - For Flashcard Review & Edits": {
      "main": [
        [
          {
            "node": "IF - Review Approved",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI - Generate Flashcards from Context": {
      "main": [
        [
          {
            "node": "JS - Parse, Validate, Dedupe & Assign SM-2 Scheduling",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Chunk Notes & Prepare Embedding Requests": {
      "main": [
        [
          {
            "node": "Wait - For Topic/Scope Selection",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Vector Similarity Search & Context Assembly": {
      "main": [
        [
          {
            "node": "AI - Generate Flashcards from Context",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "JS - Parse, Validate, Dedupe & Assign SM-2 Scheduling": {
      "main": [
        [
          {
            "node": "Wait - For Flashcard Review & Edits",
            "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.

Pro

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

About this workflow

This workflow turns submitted study notes into a reviewed flashcard deck by chunking the text, retrieving the most relevant passages with OpenAI embeddings, generating grounded Q&A cards with an OpenAI chat model, and sending the approved deck to a Slack channel. Receives notes…

Source: https://n8n.io/workflows/18210/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10
AI & RAG

This workflow transforms natural language queries into research reports through a five-stage AI pipeline. When triggered via webhook (typically from Google Sheets using the companion [](https://gist.g

Redis, Agent, Output Parser Structured +7
AI & RAG

This workflow receives a blog request via webhook, researches the topic with Tavily, generates a long-form HTML article using Google Gemini, creates two images via the kie.ai API, stores assets in Goo

Agent, @Tavily/N8N Nodes Tavily, Google Gemini Chat +6
AI & RAG

[](https://www.youtube.com/watch?v=NAn5BSr15Ks) &gt; This workflow connects a Slack chatbot with AI agents and Google Sheets to automate candidate resume evaluation. It extracts resume details, identi

HTTP Request, Output Parser Structured, OpenAI Chat +6
AI & RAG

This workflow receives first-reply LinkedIn webhook events from Aimfox, fetches the full conversation for context, uses OpenAI to classify lead intent and draft a short reply for interested leads, sen

OpenAI Chat, Chain Llm, HTTP Request +1